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.

6 min read v0.25.0

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