Upgrades
Image tags
Section titled “Image tags”.github/workflows/publish.yml pushes every image on each push to main, on v* tags and on
manual runs, with these tags:
| Tag | Meaning |
|---|---|
sha-<short> |
One exact commit; the safest thing to pin |
main |
The branch name, moves with every merge |
latest |
The default branch, same as main |
<version>, <major>.<minor> |
Release tags (v1.2.3 gives 1.2.3 and 1.2), once releases are cut |
Use the same tag for api, h5p, pdf, web and admin: they are built from one commit and
talk to each other through internal contracts. In the example that is one variable,
ULAMS_VERSION. Every image carries provenance and an SBOM.
Upgrade procedure
Section titled “Upgrade procedure”-
Read the changes since your version (commit history,
docs/ROADMAP-TODO.md, ADRs). -
Back up the databases, buckets and volumes.
-
Set the new tag in
.envand pull:Terminal window docker compose pull -
Recreate the containers:
Terminal window docker compose up -ddocker compose logs -f apiThe
apicontainer runs, in order:migrate --forceon the platform database,ulams:tenant:sync-env --migrate(rewrites every tenant env file, restores keys and runsmigrate --forcein each tenant database, one tenant after another), then the permissions seeder, then starts php-fpm and the workers. -
Run
ulams:upgrade(below) for the one-off steps of the release, check the log forsynced <host>lines and for errors, then the health endpoints (see Monitoring). A tenant that failed to migrate is reported by host; fix the cause and rerun:Terminal window docker compose exec api php artisan ulams:tenant:sync-env --migrate
ulams:upgrade: one command for the platform and every tenant
Section titled “ulams:upgrade: one command for the platform and every tenant”Start-up only migrates and rebuilds env files. Releases also need steps that must run once per
tenant: new permissions, keys, moving files, cleaning data, recreating SQL views. ulams:upgrade
runs all of them, idempotently, for the platform first and then for each active tenant:
docker compose exec api php artisan ulams:upgrade --dry-run # list what would rundocker compose exec api php artisan ulams:upgrade # run it| Step | Runs | What it does |
|---|---|---|
sync_env |
platform, every upgrade | ulams:tenant:sync-env: tenant env files, registrations and Passport keys |
h5p_export_config |
platform, every upgrade | ulams:h5p:export-config for the production H5P service (does nothing without H5P_SERVICE_CONFIG_DIR) |
migrate |
each target, every upgrade | migrate --force |
permissions |
each target, every upgrade | db:seed --class=PermissionsSeeder (adds missing permissions) |
lti_keys |
each target, every upgrade | ulams:lti:rotate-keys --init (creates the LTI key set when missing) |
cmi5_move_to_bucket |
each target, once | cmi5:move-to-bucket (when this release has the command) |
meetings_purge_frames |
each target, once | meetings:purge-frames, preceded by a logged --dry-run: deletes the webcam frames the removed meeting capture stored under consultation/{id}/{term}/{user}/ and webinar/{id}/{term}/{user}/; the images folders stay |
recreate_views |
each target, once | ulams:db:recreate-views: SQL views created before the package rename carry old class names |
“Once” steps are recorded in the platform table tenant_upgrade_steps (target is platform or
the tenant slug), so a second run skips them. A step whose command does not exist in the running
release is skipped and not recorded; it runs on the first upgrade that has it.
| Option | Effect |
|---|---|
--tenant=<slug> (repeatable) |
Only these tenants; the platform is left out |
--platform-only |
Only the platform |
--force-step=<name> (repeatable) |
Run a one-off step again |
--dry-run |
Print the plan, change and record nothing |
A failing step stops that target’s remaining steps and is reported; the other targets still run.
The command exits non-zero when any target failed, so it can gate a deploy. Tenants that are not
active (still provisioning or failed) are skipped with a note. Run it on the platform, never with
--domain.
Packages add their own steps from a service provider with
UpgradeSteps::register('name', fn (UpgradeContext $context) => $context->artisan('my:command'), since: '1.4')
(or UpgradeSteps::command(...)). Pass once: false for idempotent work that must run on every
upgrade, and make every step safe to run twice. See
New package for package conventions.
The H5P service migrates its own h5p schema in a tenant database on the first request for that
tenant after the upgrade, not at start-up.
Rolling back
Section titled “Rolling back”Migrations are not guaranteed to be reversible. To go back, restore the backup taken before the upgrade and start the previous image tag. Rolling back only the images over a migrated database is unsupported.
Infrastructure upgrades
Section titled “Infrastructure upgrades”- PostgreSQL major versions: dump and restore every database (see
Backups) or use
pg_upgrade; a new major version cannot start on the old data directory. - Valkey: drain the queues first (
DISABLE_QUEUE,DISABLE_HORIZONor simply wait), then upgrade. - Caddy: compatible within version 2; validate the Caddyfile with the new image
(
caddy validate) before switching.