diff options
Diffstat (limited to 'INSTALL.md')
| -rw-r--r-- | INSTALL.md | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..cad9c9e1 --- /dev/null +++ b/INSTALL.md | |||
| @@ -0,0 +1,333 @@ | |||
| 1 | # Installing CCCMS | ||
| 2 | |||
| 3 | A Rails 8 application on PostgreSQL. ImageMagick 7 and Ghostscript are | ||
| 4 | hard runtime dependencies: image variants, PDF thumbnails and social | ||
| 5 | cards are shelled out to them. Production runs on FreeBSD behind nginx | ||
| 6 | with Unicorn; development works anywhere the stack below does. | ||
| 7 | |||
| 8 | For the historical record of the June 2026 migration from Rails 2, see | ||
| 9 | `doc/CUTOVER_2026.md` โ it is not an installation guide and is not | ||
| 10 | maintained. | ||
| 11 | |||
| 12 | ## 1. Dependencies | ||
| 13 | |||
| 14 | | What | Why | FreeBSD 14/15 | Debian/Ubuntu | macOS (brew) | | ||
| 15 | |---|---|---|---|---| | ||
| 16 | | PostgreSQL 16 | database | `postgresql16-server postgresql16-client` | `postgresql postgresql-client libpq-dev` | `postgresql@16` | | ||
| 17 | | ImageMagick **7** | image variants, social cards | `ImageMagick7-nox11` | see trap below | `imagemagick` | | ||
| 18 | | Ghostscript | PDF rasterisation | `ghostscript10` | `ghostscript` | `ghostscript` | | ||
| 19 | | libyaml | psych | `libyaml` | `libyaml-dev` | `libyaml` | | ||
| 20 | | libffi, readline, gdbm | Ruby build | `libffi readline gdbm` | `libffi-dev libreadline-dev libgdbm-dev` | (in base) | | ||
| 21 | | libxml2, libxslt | libxml-ruby | `libxml2 libxslt` | `libxml2-dev libxslt1-dev` | `libxml2 libxslt` | | ||
| 22 | | libical | recurrence expansion via the chaos_calendar gem | libical | libical-dev | libical | | ||
| 23 | | GNU make | native gems | `gmake` | (default) | (default) | | ||
| 24 | | Node | asset pipeline | `node` | `nodejs` | `node` | | ||
| 25 | | git, curl, gnupg | fetching and verifying | `git curl gnupg` | `git curl gnupg` | (in base) | | ||
| 26 | |||
| 27 | Debian trap: the `imagemagick` package is version 6 on Debian 12 and | ||
| 28 | earlier, which has no `magick` binary, only the deprecated `convert`. | ||
| 29 | The code calls `magick` at four sites in | ||
| 30 | `app/models/concerns/file_attachment.rb`. Check with `magick -version` | ||
| 31 | before going further; if it is absent, install from a backport or build | ||
| 32 | ImageMagick 7. | ||
| 33 | |||
| 34 | FreeBSD jail: PostgreSQL needs System V shared memory. On the host, | ||
| 35 | in `/etc/jail.conf`: | ||
| 36 | |||
| 37 | allow.sysvipc = 1; | ||
| 38 | |||
| 39 | Restart the jail. Without it PostgreSQL fails to start with a cryptic | ||
| 40 | shared-memory error. | ||
| 41 | |||
| 42 | On 14.x with libical 3.0.20+ the include path for libical is | ||
| 43 | `<libical/ical.h>`, not `<ical.h>`, should the chaos_calendar Gem act | ||
| 44 | up. | ||
| 45 | |||
| 46 | ## 2. Ruby and the gemset | ||
| 47 | |||
| 48 | rvm is used for its gemsets, which work like Python venvs. Version | ||
| 49 | 3.4.10. | ||
| 50 | |||
| 51 | curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz \ | ||
| 52 | -o /tmp/rvm.tar.gz | ||
| 53 | curl -L https://github.com/rvm/rvm/releases/download/1.29.12/1.29.12.tar.gz.asc \ | ||
| 54 | -o /tmp/rvm.tar.gz.asc | ||
| 55 | gpg --keyserver hkps://keys.openpgp.org \ | ||
| 56 | --recv-keys 7D2BAF1CF37B13E2069D6956105BD0E739499BDB | ||
| 57 | gpg --verify /tmp/rvm.tar.gz.asc /tmp/rvm.tar.gz | ||
| 58 | tar -xzf /tmp/rvm.tar.gz -C /tmp | ||
| 59 | bash /tmp/rvm-1.29.12/install --auto-dotfiles | ||
| 60 | source /usr/local/rvm/scripts/rvm | ||
| 61 | |||
| 62 | **rvm 1.29.12 is the current stable release and is years old. Its | ||
| 63 | version list does not know about Ruby 3.4.** Replace it: | ||
| 64 | |||
| 65 | curl -L https://raw.githubusercontent.com/rvm/rvm/master/config/known \ | ||
| 66 | -o /usr/local/rvm/config/known | ||
| 67 | rvm list known | sed -n '/# MRI/,/^$/p' | ||
| 68 | rvm install 3.4.10 --autolibs=read-only --with-opt-dir=/usr/local | ||
| 69 | |||
| 70 | `--autolibs=read-only` stops rvm running the package manager on your | ||
| 71 | behalf. `--with-opt-dir=/usr/local` is the libyaml fix: ports and brew | ||
| 72 | install there, Ruby's configure does not look there, and without it | ||
| 73 | psych fails to build **silently** and surfaces much later as YAML errors | ||
| 74 | when Rails loads `database.yml`. Verify the build before continuing: | ||
| 75 | |||
| 76 | ruby -ryaml -ropenssl -rzlib -e 'puts "ok #{Psych::LIBYAML_VERSION}"' | ||
| 77 | |||
| 78 | Then the gemset: | ||
| 79 | |||
| 80 | cd /path/to/cccms | ||
| 81 | rvm use 3.4.10@rails8-upgrade --create | ||
| 82 | |||
| 83 | `.ruby-version` and `.ruby-gemset` in the project root make rvm switch | ||
| 84 | automatically on entering the directory. `.ruby-version` must keep the | ||
| 85 | `ruby-` prefix, `ruby-3.4.10`, not `3.4.10`, because the rc.d script | ||
| 86 | concatenates it into a gemset path and a bare version yields a path that | ||
| 87 | does not exist. | ||
| 88 | |||
| 89 | ## 3. Gems | ||
| 90 | |||
| 91 | gem install bundler | ||
| 92 | MAKE=gmake bundle install | ||
| 93 | |||
| 94 | `MAKE=gmake` on FreeBSD only, and it is not optional: several native | ||
| 95 | extensions fail against BSD make. | ||
| 96 | |||
| 97 | ## 4. Database | ||
| 98 | |||
| 99 | # FreeBSD | ||
| 100 | sysrc 'postgresql_enable="YES"' | ||
| 101 | service postgresql initdb | ||
| 102 | service postgresql start | ||
| 103 | |||
| 104 | psql -U postgres postgres | ||
| 105 | |||
| 106 | ```sql | ||
| 107 | CREATE ROLE rails WITH LOGIN PASSWORD 'choose-one'; | ||
| 108 | ALTER ROLE rails CREATEDB; | ||
| 109 | |||
| 110 | CREATE DATABASE cccms_dev OWNER rails ENCODING 'UTF8' | ||
| 111 | LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0; | ||
| 112 | CREATE DATABASE psql_test OWNER rails ENCODING 'UTF8' | ||
| 113 | LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0; | ||
| 114 | ``` | ||
| 115 | |||
| 116 | `CREATEDB` is needed because the test suite creates and drops its own | ||
| 117 | database. `TEMPLATE template0` is required whenever a non-default locale | ||
| 118 | is given. | ||
| 119 | |||
| 120 | Two config files are gitignored and must be created. `config/database.yml`: | ||
| 121 | |||
| 122 | ```yaml | ||
| 123 | development: | ||
| 124 | adapter: postgresql | ||
| 125 | encoding: unicode | ||
| 126 | database: cccms_dev | ||
| 127 | pool: 5 | ||
| 128 | username: rails | ||
| 129 | password: choose-one | ||
| 130 | |||
| 131 | test: | ||
| 132 | adapter: postgresql | ||
| 133 | encoding: UTF8 | ||
| 134 | database: psql_test | ||
| 135 | username: rails | ||
| 136 | password: | ||
| 137 | |||
| 138 | production: | ||
| 139 | adapter: postgresql | ||
| 140 | encoding: unicode | ||
| 141 | database: cccms_production | ||
| 142 | pool: 5 | ||
| 143 | username: rails | ||
| 144 | password: choose-one | ||
| 145 | ``` | ||
| 146 | |||
| 147 | `config/initializers/secret_token.rb`, one line: | ||
| 148 | |||
| 149 | ```ruby | ||
| 150 | Cccms::Application.config.secret_key_base = "<64 hex chars, e.g. from `rails secret`>" | ||
| 151 | ``` | ||
| 152 | |||
| 153 | ### 4a. Migrate. Never load the schema. | ||
| 154 | |||
| 155 | bundle exec rails db:migrate | ||
| 156 | |||
| 157 | Do not run `db:setup` or `db:schema:load`. | ||
| 158 | |||
| 159 | `db/schema.rb` is gitignored, and it could not be used even if it were | ||
| 160 | present: the full-text `search_vector` column is maintained by a PostgreSQL | ||
| 161 | trigger, and Ruby's schema format cannot express triggers. A schema-loaded | ||
| 162 | database gets the column and its GIN index with nothing populating them, | ||
| 163 | and site search then silently returns no results. Migrations are the only | ||
| 164 | complete record of the structure. | ||
| 165 | |||
| 166 | ## 5. First start | ||
| 167 | |||
| 168 | Compile the admin assets. The TinyMCE bundle lives in gitignored | ||
| 169 | `public/assets/`: | ||
| 170 | |||
| 171 | bundle exec rails assets:precompile | ||
| 172 | |||
| 173 | Bootstrap the content tree and one account: | ||
| 174 | |||
| 175 | ADMIN_PASS=choose-one bundle exec rake cccms:init | ||
| 176 | |||
| 177 | `ADMIN_LOGIN` (default `admin`) and `ADMIN_EMAIL` are optional. A missing | ||
| 178 | `ADMIN_PASS` aborts. The task creates root, the Trash, `home`, | ||
| 179 | `/updates`, `/disclosure`, `/club/erfas`, `/club/chaostreffs` with | ||
| 180 | placeholder titles, and is idempotent. | ||
| 181 | |||
| 182 | Start the server: | ||
| 183 | |||
| 184 | bundle exec rails server -p 3000 -b 0.0.0.0 | ||
| 185 | |||
| 186 | `-b 0.0.0.0` is required inside a FreeBSD jail, where `localhost` does | ||
| 187 | not resolve. | ||
| 188 | |||
| 189 | `public/system/uploads/` starts empty. It is gitignored; on a fresh | ||
| 190 | install there is nothing to copy. | ||
| 191 | |||
| 192 | ### The first admin needs two logins | ||
| 193 | |||
| 194 | The bootstrap account is an administrator without a second factor, so | ||
| 195 | it cannot yet create users, reset factors or deactivate accounts: | ||
| 196 | administrative actions need a code entered within the last thirty | ||
| 197 | minutes, and there is no password-only path. This is deliberate. To | ||
| 198 | finish: | ||
| 199 | |||
| 200 | 1. sign in as the bootstrap account | ||
| 201 | 2. **Mein Konto** -> enable second factor, scan the QR code, confirm | ||
| 202 | 3. sign out, sign in again, entering the code | ||
| 203 | |||
| 204 | Elevation is granted at that login and user management unlocks. | ||
| 205 | |||
| 206 | ## 6. Production on FreeBSD | ||
| 207 | |||
| 208 | Unicorn, started by an rc.d script. Templates in `doc/`: | ||
| 209 | |||
| 210 | doc/unicorn.rb -> /usr/local/etc/unicorn.rb | ||
| 211 | doc/rc.d_cccms -> /usr/local/etc/rc.d/cccms | ||
| 212 | |||
| 213 | The rc.d script reads `.ruby-version` and `.ruby-gemset` from the project | ||
| 214 | directory to find the gemset โ see the prefix note in ยง2. | ||
| 215 | |||
| 216 | nginx proxies everything to Unicorn. Uploads need their own block: | ||
| 217 | |||
| 218 | location /system/uploads/ { | ||
| 219 | add_header Content-Security-Policy "sandbox" always; | ||
| 220 | add_header X-Content-Type-Options "nosniff" always; | ||
| 221 | |||
| 222 | proxy_pass http://127.0.0.1:9090; | ||
| 223 | proxy_set_header Host $host; | ||
| 224 | proxy_buffering off; | ||
| 225 | proxy_set_header X-Forwarded-Host $host; | ||
| 226 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 227 | } | ||
| 228 | |||
| 229 | location / { | ||
| 230 | proxy_pass http://127.0.0.1:9090/; | ||
| 231 | proxy_set_header Host $host; | ||
| 232 | proxy_buffering off; | ||
| 233 | proxy_set_header X-Forwarded-Host $host; | ||
| 234 | proxy_set_header X-Forwarded-Proto $scheme; | ||
| 235 | } | ||
| 236 | |||
| 237 | - Note: No trailing slash on its `proxy_pass`. With one, nginx strips the | ||
| 238 | matched prefix and the backend 404s. The `location /` block gets away | ||
| 239 | with a trailing slash only because replacing `/` with `/` is a no-op. | ||
| 240 | - The CSP is not optional. Uploaded files are served by Rails' static | ||
| 241 | file server, which bypasses the middleware that sets the application's | ||
| 242 | security headers. Without `sandbox`, an uploaded SVG opened directly is | ||
| 243 | a document that runs its own script, on the same origin as the site | ||
| 244 | and its admin sessions. | ||
| 245 | - `add_header` in a location replaces inherited headers, so anything | ||
| 246 | set at server level must be repeated here. | ||
| 247 | |||
| 248 | ## 7. Maintenance | ||
| 249 | |||
| 250 | ### Deploy | ||
| 251 | |||
| 252 | service cccms stop && git pull && bundle exec rails db:migrate && service cccms start | ||
| 253 | |||
| 254 | `bundle install` too when `Gemfile.lock` changed. Use `install over` | ||
| 255 | `update`: the lockfile names exact versions and checksums, so the server | ||
| 256 | gets what was tested. In development, `touch tmp/restart.txt` restarts a | ||
| 257 | running server in place. | ||
| 258 | |||
| 259 | Occurrences are regenerated yearly at service start. Recurring | ||
| 260 | events are expanded into finite `occurrences` rows rather than computed | ||
| 261 | per request. Range queries over 200+ recurring events would otherwise | ||
| 262 | mean full RRULE expansion on every page load. The window is five years, | ||
| 263 | which is chaos_calendar's expansion limit. | ||
| 264 | |||
| 265 | The rc.d script's `start_postcmd` regenerates when | ||
| 266 | `/var/db/cccms_occurrences_regenerated` is missing or older than 365 | ||
| 267 | days. Run at post-start, since it must not block the server coming up | ||
| 268 | or run when startup failed. | ||
| 269 | |||
| 270 | service cccms regenerate_occurrences | ||
| 271 | |||
| 272 | The yearly cadence is chosen to coincide with the reboot that follows an | ||
| 273 | operating-system upgrade. Regeneration is expensive, and that is the | ||
| 274 | natural point to pay for it. | ||
| 275 | |||
| 276 | ### Security updates | ||
| 277 | |||
| 278 | gem install bundler-audit # once, outside the Gemfile | ||
| 279 | bundle-audit check --update | ||
| 280 | |||
| 281 | Worth running monthly. Vulnerabilities in the HTML sanitizer matter most | ||
| 282 | here: every page body passes through it. | ||
| 283 | |||
| 284 | Ruby upgrades: a new gemset rather than a replacement, so the old one | ||
| 285 | remains as the way back. Install and populate the new gemset before | ||
| 286 | pulling a commit that changes `.ruby-version`, or every `rake` and | ||
| 287 | `runner` invocation breaks while the running server carries on under the | ||
| 288 | old Ruby. | ||
| 289 | |||
| 290 | ### One-shot tasks | ||
| 291 | |||
| 292 | - `users:clear_otp` is the lockout escape hatch: it clears one account's | ||
| 293 | second factor from the shell when every administrator is locked out. | ||
| 294 | Deliberately unwitnessed โ there is no actor to attribute a shell | ||
| 295 | command to. | ||
| 296 | |||
| 297 | Logs are in `log/`, gitignored. The action log inside the application at | ||
| 298 | `/admin/log` records who changed what; `log/production.log` records | ||
| 299 | everything else. | ||
| 300 | |||
| 301 | ## 8. Traps | ||
| 302 | |||
| 303 | - ImageMagick's policy travels with the project. | ||
| 304 | `config/imagemagick/policy.xml` is loaded via `MAGICK_CONFIGURE_PATH`, | ||
| 305 | set per invocation. Nothing to install, and do not patch the system | ||
| 306 | `policy.xml` or a port upgrade would revert it and a fresh checkout | ||
| 307 | would not have it. ImageMagick prepends the project path, so the | ||
| 308 | system file is still read. | ||
| 309 | - Two independent allowlists govern editor HTML. TinyMCE's | ||
| 310 | `extended_valid_elements` in `public/javascripts/admin_interface.js` | ||
| 311 | and the server's sanitizer in `ContentHelper#aggregate?`. An attribute | ||
| 312 | permitted by one and not the other is either offered and discarded, or | ||
| 313 | stripped from markup the application itself emits. They must be | ||
| 314 | changed together. | ||
| 315 | - `otp_required` is `false` on every account. Second factors are | ||
| 316 | effectively opt-in until that is flipped, and flipping it locks out | ||
| 317 | anyone who has not enrolled. | ||
| 318 | - Uploads are not in the repository. `public/system/` is gitignored | ||
| 319 | and is not covered by a database dump either. Back it up separately or | ||
| 320 | the site loses every image. | ||
| 321 | - The test database is not sandboxed against `rails runner`. A `runner` | ||
| 322 | invocation that writes will leave rows behind. Wrap writes in a | ||
| 323 | transaction with `raise ActiveRecord::Rollback`, or run | ||
| 324 | `RAILS_ENV=test bundle exec rails db:test:prepare` afterwards. | ||
| 325 | - Ruby 3.4 bundled gems are fatal under bundler. A `require` of a | ||
| 326 | gem that is bundled-but-not-default warns outside bundler and raises | ||
| 327 | `LoadError` under `bundle exec`. `csv` is already declared for this | ||
| 328 | reason; the same applies to `base64`, `bigdecimal` and friends if a | ||
| 329 | future `require` reaches for one. | ||
| 330 | |||
| 331 | ## Tests | ||
| 332 | |||
| 333 | bundle exec rake test | ||
