From a0a495d804319c3f7ad179baba7b87b41f1dc1f3 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 4 Aug 2026 21:11:25 +0200 Subject: Rework the README/INSTALL documents and provide a bootstrap script (untested ;) --- INSTALL.md | 333 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 INSTALL.md (limited to 'INSTALL.md') diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..cad9c9e1 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,333 @@ +# Installing CCCMS + +A Rails 8 application on PostgreSQL. ImageMagick 7 and Ghostscript are +hard runtime dependencies: image variants, PDF thumbnails and social +cards are shelled out to them. Production runs on FreeBSD behind nginx +with Unicorn; development works anywhere the stack below does. + +For the historical record of the June 2026 migration from Rails 2, see +`doc/CUTOVER_2026.md` โ€” it is not an installation guide and is not +maintained. + +## 1. Dependencies + +| What | Why | FreeBSD 14/15 | Debian/Ubuntu | macOS (brew) | +|---|---|---|---|---| +| PostgreSQL 16 | database | `postgresql16-server postgresql16-client` | `postgresql postgresql-client libpq-dev` | `postgresql@16` | +| ImageMagick **7** | image variants, social cards | `ImageMagick7-nox11` | see trap below | `imagemagick` | +| Ghostscript | PDF rasterisation | `ghostscript10` | `ghostscript` | `ghostscript` | +| libyaml | psych | `libyaml` | `libyaml-dev` | `libyaml` | +| libffi, readline, gdbm | Ruby build | `libffi readline gdbm` | `libffi-dev libreadline-dev libgdbm-dev` | (in base) | +| libxml2, libxslt | libxml-ruby | `libxml2 libxslt` | `libxml2-dev libxslt1-dev` | `libxml2 libxslt` | +| libical | recurrence expansion via the chaos_calendar gem | libical | libical-dev | libical | +| GNU make | native gems | `gmake` | (default) | (default) | +| Node | asset pipeline | `node` | `nodejs` | `node` | +| git, curl, gnupg | fetching and verifying | `git curl gnupg` | `git curl gnupg` | (in base) | + +Debian trap: the `imagemagick` package is version 6 on Debian 12 and +earlier, which has no `magick` binary, only the deprecated `convert`. +The code calls `magick` at four sites in +`app/models/concerns/file_attachment.rb`. Check with `magick -version` +before going further; if it is absent, install from a backport or build +ImageMagick 7. + +FreeBSD jail: PostgreSQL needs System V shared memory. On the host, +in `/etc/jail.conf`: + + allow.sysvipc = 1; + +Restart the jail. Without it PostgreSQL fails to start with a cryptic +shared-memory error. + +On 14.x with libical 3.0.20+ the include path for libical is +``, not ``, should the chaos_calendar Gem act +up. + +## 2. Ruby and the gemset + +rvm is used for its gemsets, which work like Python venvs. Version +3.4.10. + + curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz \ + -o /tmp/rvm.tar.gz + curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz.asc \ + -o /tmp/rvm.tar.gz.asc + gpg --keyserver hkps://keys.openpgp.org \ + --recv-keys 7D2BAF1CF37B13E2069D6956105BD0E739499BDB + gpg --verify /tmp/rvm.tar.gz.asc /tmp/rvm.tar.gz + tar -xzf /tmp/rvm.tar.gz -C /tmp + bash /tmp/rvm-1.29.12/install --auto-dotfiles + source /usr/local/rvm/scripts/rvm + +**rvm 1.29.12 is the current stable release and is years old. Its +version list does not know about Ruby 3.4.** Replace it: + + curl -L https://raw.githubusercontent.com/rvm/rvm/master/config/known \ + -o /usr/local/rvm/config/known + rvm list known | sed -n '/# MRI/,/^$/p' + rvm install 3.4.10 --autolibs=read-only --with-opt-dir=/usr/local + +`--autolibs=read-only` stops rvm running the package manager on your +behalf. `--with-opt-dir=/usr/local` is the libyaml fix: ports and brew +install there, Ruby's configure does not look there, and without it +psych fails to build **silently** and surfaces much later as YAML errors +when Rails loads `database.yml`. Verify the build before continuing: + + ruby -ryaml -ropenssl -rzlib -e 'puts "ok #{Psych::LIBYAML_VERSION}"' + +Then the gemset: + + cd /path/to/cccms + rvm use 3.4.10@rails8-upgrade --create + +`.ruby-version` and `.ruby-gemset` in the project root make rvm switch +automatically on entering the directory. `.ruby-version` must keep the +`ruby-` prefix, `ruby-3.4.10`, not `3.4.10`, because the rc.d script +concatenates it into a gemset path and a bare version yields a path that +does not exist. + +## 3. Gems + + gem install bundler + MAKE=gmake bundle install + +`MAKE=gmake` on FreeBSD only, and it is not optional: several native +extensions fail against BSD make. + +## 4. Database + + # FreeBSD + sysrc 'postgresql_enable="YES"' + service postgresql initdb + service postgresql start + + psql -U postgres postgres + +```sql +CREATE ROLE rails WITH LOGIN PASSWORD 'choose-one'; +ALTER ROLE rails CREATEDB; + +CREATE DATABASE cccms_dev OWNER rails ENCODING 'UTF8' + LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0; +CREATE DATABASE psql_test OWNER rails ENCODING 'UTF8' + LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0; +``` + +`CREATEDB` is needed because the test suite creates and drops its own +database. `TEMPLATE template0` is required whenever a non-default locale +is given. + +Two config files are gitignored and must be created. `config/database.yml`: + +```yaml +development: + adapter: postgresql + encoding: unicode + database: cccms_dev + pool: 5 + username: rails + password: choose-one + +test: + adapter: postgresql + encoding: UTF8 + database: psql_test + username: rails + password: + +production: + adapter: postgresql + encoding: unicode + database: cccms_production + pool: 5 + username: rails + password: choose-one +``` + +`config/initializers/secret_token.rb`, one line: + +```ruby +Cccms::Application.config.secret_key_base = "<64 hex chars, e.g. from `rails secret`>" +``` + +### 4a. Migrate. Never load the schema. + + bundle exec rails db:migrate + +Do not run `db:setup` or `db:schema:load`. + +`db/schema.rb` is gitignored, and it could not be used even if it were +present: the full-text `search_vector` column is maintained by a PostgreSQL +trigger, and Ruby's schema format cannot express triggers. A schema-loaded +database gets the column and its GIN index with nothing populating them, +and site search then silently returns no results. Migrations are the only +complete record of the structure. + +## 5. First start + +Compile the admin assets. The TinyMCE bundle lives in gitignored +`public/assets/`: + + bundle exec rails assets:precompile + +Bootstrap the content tree and one account: + + ADMIN_PASS=choose-one bundle exec rake cccms:init + +`ADMIN_LOGIN` (default `admin`) and `ADMIN_EMAIL` are optional. A missing +`ADMIN_PASS` aborts. The task creates root, the Trash, `home`, +`/updates`, `/disclosure`, `/club/erfas`, `/club/chaostreffs` with +placeholder titles, and is idempotent. + +Start the server: + + bundle exec rails server -p 3000 -b 0.0.0.0 + +`-b 0.0.0.0` is required inside a FreeBSD jail, where `localhost` does +not resolve. + +`public/system/uploads/` starts empty. It is gitignored; on a fresh +install there is nothing to copy. + +### The first admin needs two logins + +The bootstrap account is an administrator without a second factor, so +it cannot yet create users, reset factors or deactivate accounts: +administrative actions need a code entered within the last thirty +minutes, and there is no password-only path. This is deliberate. To +finish: + +1. sign in as the bootstrap account +2. **Mein Konto** -> enable second factor, scan the QR code, confirm +3. sign out, sign in again, entering the code + +Elevation is granted at that login and user management unlocks. + +## 6. Production on FreeBSD + +Unicorn, started by an rc.d script. Templates in `doc/`: + + doc/unicorn.rb -> /usr/local/etc/unicorn.rb + doc/rc.d_cccms -> /usr/local/etc/rc.d/cccms + +The rc.d script reads `.ruby-version` and `.ruby-gemset` from the project +directory to find the gemset โ€” see the prefix note in ยง2. + +nginx proxies everything to Unicorn. Uploads need their own block: + + location /system/uploads/ { + add_header Content-Security-Policy "sandbox" always; + add_header X-Content-Type-Options "nosniff" always; + + proxy_pass http://127.0.0.1:9090; + proxy_set_header Host $host; + proxy_buffering off; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://127.0.0.1:9090/; + proxy_set_header Host $host; + proxy_buffering off; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + } + +- Note: No trailing slash on its `proxy_pass`. With one, nginx strips the + matched prefix and the backend 404s. The `location /` block gets away + with a trailing slash only because replacing `/` with `/` is a no-op. +- The CSP is not optional. Uploaded files are served by Rails' static + file server, which bypasses the middleware that sets the application's + security headers. Without `sandbox`, an uploaded SVG opened directly is + a document that runs its own script, on the same origin as the site + and its admin sessions. +- `add_header` in a location replaces inherited headers, so anything + set at server level must be repeated here. + +## 7. Maintenance + +### Deploy + + service cccms stop && git pull && bundle exec rails db:migrate && service cccms start + +`bundle install` too when `Gemfile.lock` changed. Use `install over` +`update`: the lockfile names exact versions and checksums, so the server +gets what was tested. In development, `touch tmp/restart.txt` restarts a +running server in place. + +Occurrences are regenerated yearly at service start. Recurring +events are expanded into finite `occurrences` rows rather than computed +per request. Range queries over 200+ recurring events would otherwise +mean full RRULE expansion on every page load. The window is five years, +which is chaos_calendar's expansion limit. + +The rc.d script's `start_postcmd` regenerates when +`/var/db/cccms_occurrences_regenerated` is missing or older than 365 +days. Run at post-start, since it must not block the server coming up +or run when startup failed. + + service cccms regenerate_occurrences + +The yearly cadence is chosen to coincide with the reboot that follows an +operating-system upgrade. Regeneration is expensive, and that is the +natural point to pay for it. + +### Security updates + + gem install bundler-audit # once, outside the Gemfile + bundle-audit check --update + +Worth running monthly. Vulnerabilities in the HTML sanitizer matter most +here: every page body passes through it. + +Ruby upgrades: a new gemset rather than a replacement, so the old one +remains as the way back. Install and populate the new gemset before +pulling a commit that changes `.ruby-version`, or every `rake` and +`runner` invocation breaks while the running server carries on under the +old Ruby. + +### One-shot tasks + +- `users:clear_otp` is the lockout escape hatch: it clears one account's + second factor from the shell when every administrator is locked out. + Deliberately unwitnessed โ€” there is no actor to attribute a shell + command to. + +Logs are in `log/`, gitignored. The action log inside the application at +`/admin/log` records who changed what; `log/production.log` records +everything else. + +## 8. Traps + +- ImageMagick's policy travels with the project. + `config/imagemagick/policy.xml` is loaded via `MAGICK_CONFIGURE_PATH`, + set per invocation. Nothing to install, and do not patch the system + `policy.xml` or a port upgrade would revert it and a fresh checkout + would not have it. ImageMagick prepends the project path, so the + system file is still read. +- Two independent allowlists govern editor HTML. TinyMCE's + `extended_valid_elements` in `public/javascripts/admin_interface.js` + and the server's sanitizer in `ContentHelper#aggregate?`. An attribute + permitted by one and not the other is either offered and discarded, or + stripped from markup the application itself emits. They must be + changed together. +- `otp_required` is `false` on every account. Second factors are + effectively opt-in until that is flipped, and flipping it locks out + anyone who has not enrolled. +- Uploads are not in the repository. `public/system/` is gitignored + and is not covered by a database dump either. Back it up separately or + the site loses every image. +- The test database is not sandboxed against `rails runner`. A `runner` + invocation that writes will leave rows behind. Wrap writes in a + transaction with `raise ActiveRecord::Rollback`, or run + `RAILS_ENV=test bundle exec rails db:test:prepare` afterwards. +- Ruby 3.4 bundled gems are fatal under bundler. A `require` of a + gem that is bundled-but-not-default warns outside bundler and raises + `LoadError` under `bundle exec`. `csv` is already declared for this + reason; the same applies to `base64`, `bigdecimal` and friends if a + future `require` reaches for one. + +## Tests + + bundle exec rake test -- cgit v1.3