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.
blog/migrations-without-fearMost 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:
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-runYou read the plan, confirm the target is right, then drop --dry-run to go:
orchstep run migrate --var target=productionWhat you actually gained
| Concern | Hand-run runbook | OrchStep migrate task |
|---|---|---|
| Backup before apply | "don't forget to dump first" | a backup step with a recorded path |
| Rollback on failure | manual, under pressure | catch: restores steps.backup.snapshot |
| Always notify | someone remembers | finally: fires either way |
| Preview against prod | run it and hope | --dry-run prints the plan |
| Where it lives | a wiki page | the 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
- Error Handling —
catch,finally, retry, and timeouts in depth - Previewing with Dry Run — resolve and inspect a plan without running it
- Variables & Outputs — passing
steps.*between steps - shell — the function doing the real work here
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.
curl -fsSL https://orchstep.dev/install.sh | sh