Berth

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:

  1. backs up each Postgres database the app is linked to,
  2. runs the command in a fresh container from the new image, with the new release's config (so DATABASE_URL and friends are there), and streams the output,
  3. 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).

Edit this page on GitHub (it's a section of the README)