Migrations
Tell Berth how your app migrates its database and it'll run that once per
release, after the build and before the new code sees any traffic. Add a
Procfile to the repo with a release: line:
release: npx prisma migrate deploy
Any tool works: alembic upgrade head, python manage.py migrate --noinput,
bin/rails db:migrate, npx knex migrate:latest, or a script of your own.
No Procfile? Set it as config instead:
berth config:set myapp BERTH_RELEASE_COMMAND="alembic upgrade head".
Then on every deploy Berth:
- backs up each Postgres database the app is linked to,
- runs the command in a fresh container from the new image, with the new
release's config (so
DATABASE_URLand friends are there), and streams the output, - starts the new release only if the command worked. If it didn't, the deploy stops, the running version carries on serving, and the output tells you what broke and where the backup is.
-----> Backed up pg:chirpdb before the release command (0.2s, 5.1 KB)
-----> Running release command: node migrate.js
applied 004_add_mood (3ms)
1 migration(s) applied
-----> Release command finished in 0.8s
-----> Starting berth-chirp-5-k2x9 on 127.0.0.1:8002
berth logs myapp --release shows the latest run. If your app creates its
tables when it starts up, move that into the release command. Otherwise a
half-done migration leaves you with a crashing container and nothing to undo
it with. Config changes (config:set, pg:link) run the command again too,
so it has to be safe to repeat. Every migration tool's "apply what's pending"
command is.
If Berth spots a migration tool (Prisma, Drizzle, Knex, Alembic, Django, Rails) but no release command, the deploy prints the line to add. It won't run migrations you didn't ask for.
Write migrations the old code can live with
Because deploys have no downtime, the old release is still serving for a few seconds against the migrated database. So every migration has to work with both the old code and the new:
- Adding a column or table is fine. Make new columns nullable or give them a default.
- Removing one takes two releases: stop using it, then drop it later.
- Renaming is add, copy, switch the code, then drop.
Try it on the dev version first
The dev version runs the release command against its own database, so you can try a migration before committing it:
berth dev:db:copy myapp # copy production's data into the dev database, then migrate it
berth dev:push # build your working tree, run the release command, start it
dev:push exits with code 3 if the release command fails, with the error in
the output (and in --json). This is the loop to give an agent: write the
migration, dev:push, fix until it exits 0, commit.
Rolling back data
Since there's a backup from just before each release command, you can undo a migration along with the code:
berth rollback myapp --with-data
This swaps back to the previous release, then replaces the database with the
backup taken just before the newer release migrated it. Anything written
since then is gone, so Berth asks first, and saves the current data to
~/.berth/backups/pg/<db>/ before restoring. Without --with-data, a
rollback leaves the data alone, which is what you want whenever the migration
was backward compatible.
The last 14 pre-release backups per app are kept in
~/.berth/backups/pg/<db>/releases/<app>/. If a database is too big to dump
on every deploy, berth config:set myapp BERTH_RELEASE_BACKUP=off skips it
(and --with-data then has nothing to restore).