Running Database Migrations Safely on Every Deploy
release: runs commands inside the new container before traffic switches — a failed migration rolls back instead of going live broken.
The gap hooks: couldn't fill
Neo already had hooks: pre_build and hooks: post_deploy — but those run on your machine, before or after the deploy. They can't run php artisan migrate --force inside the container that's about to serve traffic, which is what a migration actually needs.
release: fills that gap:
release:
- php artisan migrate --force
- php artisan config:cache
Where and when it runs
The commands execute inside the new container (app-<name>-next), after its health check passes, and — critically — before Caddy switches traffic to it. If any command exits non-zero, the new container is removed and the deploy aborts. The old version keeps serving, untouched. That rollback is what makes automating migrate --force safe in the first place.
Per-path behavior
| Path | Behavior |
|---|---|
| Normal deploy | Runs in -next, before the traffic switch; failure rolls back |
Scaled app (scale: 3) |
Runs once, in the first new replica — migrations aren't safe to run concurrently |
--env-only |
Runs against the live container (no -next exists on this path) — failure is reported, not rolled back |
--all |
Runs per environment, before that environment's switch — one environment failing doesn't take the others down |
Per-environment overrides
An environment's release: list fully replaces the top-level one, the same rule as hooks::
release:
- php artisan migrate --force
environments:
staging:
release:
- php artisan migrate --force
- php artisan db:seed --class=DemoSeeder
What release: is not for
Commands here must exit. A queue worker or a long-running process started as a release: command will stall every future deploy waiting for it to finish — that belongs in workers: instead.
Try it yourself — Neo is free, with every feature unlocked.
Get started free