The shape of an upgrade
Four things happen, and the order is not interchangeable:- New code arrives (new image, or new checkout plus
composer install) - Landlord migrations run — platform-wide tables
- Tenant migrations run — once per tenant schema
- Queue workers restart, so they stop running the previous release from memory
Docker Compose
horizon:terminate lets in-flight jobs finish and then exits; Compose restarts
the container with the new image. Killing workers outright drops whatever they
were processing — and for an inbound webhook that means a message the provider
already considers delivered.
Pin a version rather than tracking latest if you want upgrades to be a decision
rather than an event:
Two schemas, two migration commands
Tenant isolation is schema-per-tenant, so a migration touching tenant tables must run once per tenant.ops:tenants-migrate iterates active tenants; plain
artisan migrate does not and will leave every tenant schema untouched.
A release can contain either kind or both. Running both commands unconditionally
is safe — an already-applied migration is skipped.
Before upgrading
Back up the database. Migrations are not reversible in practice:down()
methods exist but are exercised far less than up(), and a partial rollback on a
schema-per-tenant layout is worse than a restore.
Downtime
The application tolerates a short window where old and new run side by side — webhook ingress answers, and work queues up rather than being lost. What it does not tolerate is old workers against a migrated schema, which is why they restart last. For a zero-downtime upgrade the ordering is: start new web containers, migrate, then restart workers. Anything more elaborate is untested territory here.The gateway
The gateway upgrades independently of the application — it shares only Redis and the queue format, both versioned.failed_jobs rather than processing it with half
its context. Release notes call this out when it applies.