Deploying and CI
mmk is a normal command that exits 0 when everything worked and 1 when it didn't, so it fits into any pipeline. This page covers the setups people use most.
Where should migrations run?
There are two common places, and either is fine:
- A deploy step, before the new version starts. The pipeline runs
mmk up. If it fails, the deploy stops and the old version keeps running. This is the easiest to reason about. - When the app starts. The app calls
runMigrations()before it starts listening. Handy for serverless and small apps with no pipeline. See Running from your code.
Whichever you choose, the lock makes sure only one run happens at a time, even if two deploys or ten app instances start together.
Run migrations as a deploy step
npx mmk upThat's the whole step. Use environment variables for the connection, so no secrets sit in your repo:
MMK_URI="$MONGO_URL" MMK_DB=shop npx mmk upBlock a deploy while migrations are pending
If migrations run somewhere else (a separate job, or by hand) and you want the app deploy to refuse to go ahead until they've run, use status --check. It exits 1 when anything is pending:
npx mmk status --check || { echo "Run the migrations first"; exit 1; }This check doesn't need write access: status, list and dry-run work with a MongoDB user that has only the read role. See With a read-only database user.
GitHub Actions
A deploy job that runs migrations on every push to main:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
env:
MMK_URI: ${{ secrets.MONGO_URL }}
MMK_DB: shop
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx mmk dry-run up # writes the plan into the job log
- run: npx mmk up
deploy:
needs: migrate # only deploys if the migrations worked
runs-on: ubuntu-latest
steps:
- run: echo "deploy the app here"The dry-run step changes nothing. It's there so the job log shows what was about to run, which helps a lot when you look back at a deploy weeks later.
Test every migration on pull requests
It's worth checking that each migration can run from scratch and roll back, before it gets anywhere near production. A MongoDB service container makes this cheap:
# .github/workflows/migrations.yml
name: Migrations
on: pull_request
jobs:
up-down-up:
runs-on: ubuntu-latest
services:
mongo:
image: mongo:7
ports: ['27017:27017']
env:
MMK_URI: mongodb://localhost:27017
MMK_DB: ci_check
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx mmk up # everything applies on an empty database
- run: npx mmk down --steps 1 # the newest migration rolls back
- run: npx mmk up # and applies againThis catches the most common mistakes: a typo in down, an index name that doesn't match, a migration that only works on a database that already has some data.
Transactions in CI
A plain mongo service container is a standalone server, and standalone servers don't support transactions. If your migrations use useTransaction, start the container as a single-node replica set, or run your tests with mongodb-memory-server's MongoMemoryReplSet.
Docker
If your container runs migrations before it starts the app, put both in the entrypoint:
#!/bin/sh
# entrypoint.sh
set -e
npx mmk up # stop here if a migration fails
exec node server.jsInside Docker Compose or Kubernetes, the database host is usually the service name, not localhost:
docker run --rm \
-e MMK_URI="mongodb://mongo:27017" \
-e MMK_DB=shop \
my-app npx mmk upIn Kubernetes, a Job (or a Helm pre-upgrade hook) that runs npx mmk up before the new pods roll out works the same way as a pipeline step.
Production images and devDependencies
Many Dockerfiles run npm ci --omit=dev. If mongo-migrate-kit is only a devDependency, the image has no mmk, and in a non-interactive shell npx mmk will try to download an unrelated package that happens to be called mmk, with your database credentials in the environment. Put mongo-migrate-kit in dependencies and run it through an npm script ("migrate": "mmk up", then npm run migrate), which never downloads anything.
Reading the output in scripts
Add --json to any command that reads or changes migrations and you get one JSON document on stdout. Progress lines and the spinner move to stderr, and so does anything your own code prints (a console.log in a migration, a hook or a config function), so stdout is safe to pipe:
npx mmk up --json[
{
"file": "20261012091500-backfill-order-status.js",
"status": "applied",
"duration": 265,
"batch": 2
}
]A few one-liners with jq:
# how many migrations are pending?
npx mmk list --pending --json | jq length
# names of the files this run applied
npx mmk up --json | jq -r '.[] | select(.status == "applied") | .file'
# files edited after they ran, with the checksum each one ran with
npx mmk status --json | jq -r '.[] | select(.checksumOk == false) | "\(.file) \(.checksum)"'The checksum is the SHA-256 of the file's text, so sha256sum <file> (or git show <commit>:<file> | sha256sum) gives a value you can compare with it.
When a command fails in JSON mode, it prints { "error": { "code": "...", "message": "..." } } and exits 1. The code values are listed in Error codes.
When up, down or redo fails partway, error.details.results says what ran before it stopped, in the same shape as a successful run. The file that failed comes last, with "status": "error":
# which files did the failed run apply?
npx mmk up --json | jq -r '.error?.details.results[]? | select(.status == "applied") | .file'Tips
- Use a Node version that can load your files. Node 18 or newer for
.js. For.tswithout a loader, Node 22.18 or newer. - Log the plan.
mmk dry-run upbeforemmk upcosts a second and makes the job log much more useful later. - Killed jobs can leave a lock behind. It frees itself after
lockTTLSeconds(60 by default). If you can't wait,npx mmk unlock --yesclears it. Only do that when you're sure nothing is running.
Next
Running from your code shows runMigrations() for app start-up and serverless, and the full API.