BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

Database migrations without the 2am dread

A migration is the one deploy you can't easily undo — so wrap it in a backup, a rollback that fires on failure, and a notification that always runs. One OrchStep task, previewable before it touches the database.

Jun 23, 2026 OrchStep Team 6 minROLE: Backend DeveloperSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/migrations-without-fear
VIEW SOURCE

Most deploys are reversible. You ship a bad build, you roll back to the last image, done. Database migrations are the exception: by the time ALTER TABLE has run, the old schema is gone. That's why the migration is the step people run by hand, at 2am, holding their breath — and why the runbook lives in someone's head instead of in the repo.

The runbook is actually simple. Check you can reach the database. Take a backup. Apply the migration. If it fails, restore the backup. Either way, tell the team. The problem was never the steps — it was that they lived in a wiki page nobody runs the same way twice. Here they are as one task.

The migrate task

catch: runs only if the step fails; finally: runs no matter what. That's the whole safety net — rollback on failure, notify always — and it reads top to bottom:

orchstep.yml
name: db
defaults:
  to: latest
  target: staging

tasks:
  # `orchstep run migrate --var target=production --dry-run`
  migrate:
    desc: "Migrate the database with backup + rollback"
    steps:
      - name: precheck
        func: shell
        do: echo "checking connectivity to {{ vars.target }} database"

      - name: backup
        func: shell
        do: echo "snapshotting {{ vars.target }} before migrating to {{ vars.to }}"
        outputs:
          snapshot: "backup-{{ vars.target }}.sql"

      - name: apply
        func: shell
        do: echo "applying migrations up to {{ vars.to }}"
        catch:
          - name: rollback
            func: shell
            do: echo "migration failed — restoring {{ steps.backup.snapshot }}"
        finally:
          - name: notify
            func: shell
            do: echo "migration of {{ vars.target }} finished (notifying #ops)"

      - name: verify
        func: shell
        do: echo "verifying schema version is {{ vars.to }} on {{ vars.target }}"

(Every step echos, so the demo runs anywhere. Swap in your real pg_dump, your migration tool, and a Slack curl to make it live.)

Read the safety net out loud

The backup step records where it put the snapshot in outputs.snapshot, and the rollback inside catch: reads it back as steps.backup.snapshot. The restore always points at the backup this exact run took — not a hard-coded path that drifted three migrations ago.

apply is the only step that can plausibly blow up, so it's the only one with a catch:. If it throws, rollback runs; if it succeeds, rollback is skipped. Either way finally.notify fires, so the channel hears about every migration, not just the ones someone remembered to announce.

See it before it runs

This is the step that turns 2am dread into a Tuesday afternoon. A dry-run resolves the variables, walks the plan, and prints exactly what would happen against production — without opening a connection:

orchstep run migrate --var target=production --dry-run

You read the plan, confirm the target is right, then drop --dry-run to go:

orchstep run migrate --var target=production

What you actually gained

ConcernHand-run runbookOrchStep migrate task
Backup before apply"don't forget to dump first"a backup step with a recorded path
Rollback on failuremanual, under pressurecatch: restores steps.backup.snapshot
Always notifysomeone remembersfinally: fires either way
Preview against prodrun it and hope--dry-run prints the plan
Where it livesa wiki pagethe repo, in version control

Where OrchStep is not the answer

If your migration tool already does transactional, auto-rolling-back migrations end to end, lean on it — this pattern shines when the safe sequence spans more than the migration itself: the snapshot, the verify, the notification, the human-readable preview. That orchestration is the part that was living in someone's head.

Where to go next

Got a migration runbook that only lives in your head? Put it in the repo as a task you can dry-run. Install the binary and start with the demo above.

#DATABASE#MIGRATIONS#ERROR-HANDLING#TASK-RUNNER
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY