Upgrading or migrating MongoDB should be boring: predictable plans, repeatable checks, and safe fallbacks. This guide gives you a practical path to:
- Plan a version upgrade or data migration with low risk
- Back up and rehearse restores
- Validate changes with code and queries
- Roll back cleanly if something fails
You will see a production-oriented workflow and a local pilot you can run with Docker, Node.js, and a minimal Express API.
Workflow Overview
The safest way to upgrade is to split the work into clear, checkable steps. Treat each step as a pass or fail gate and proceed only when the previous step is green.
1) Define scope and success
- Target: server version(s), driver versions, and any schema or index changes
- Constraints: allowed downtime (ideally zero or near zero for replica sets), maintenance window, data size
- Success: measurable health checks, application smoke tests, and performance thresholds
Example success criteria:
- All nodes on new version
- Feature compatibility set to the new version
- App smoke tests pass (connect, read, write, index use)
- Errors and slow queries do not increase over baseline
2) Inventory and compatibility
Collect what you have today:
- Topology: standalone, replica set, or sharded cluster
- Versions: MongoDB, feature compatibility, and drivers
- Storage and disk headroom
- Critical ops: large collections, capped collections, TTL indexes, text and geo indexes
Useful checks in mongosh:
// Current feature compatibility version
db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })
// Server build info
db.version()
db.serverBuildInfo()
Driver readiness (Node.js example):
- Use a driver version that supports the target server
- Enable retryable writes when appropriate
- Set sensible timeouts and connection pool sizes
3) Backups and restore rehearsal
Take a consistent backup and prove you can restore it before touching production.
# Logical backup (portable across versions)
mongodump --uri "mongodb://user: pass@host:27017/dbname" \
--out ./backup-$(date +%Y%m%d-%H%M)
# Restore rehearsal into a throwaway database
mongorestore --uri "mongodb://user: pass@staging:27017/rehearsal" \
./backup-YYYYMMDD-HHMM
Also consider filesystem snapshots if your storage platform supports them. Keep at least one verified snapshot per node.
4) Staging environment
Mirror production settings as closely as possible:
- Same major MongoDB version as production baseline
- Same auth mode, TLS settings, and indexes
- Representative data volume (subset is fine)
Load seed data using mongorestore, or let the application generate it.
5) Application readiness (Node.js + Express)
Prepare the app for rolling changes and transient errors.
// app.js
const express = require('express');
const { MongoClient } = require('mongodb');
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/appdb';
async function start() {
const client = new MongoClient(MONGODB_URI, {
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000,
retryWrites: true
});
await client.connect();
const db = client.db();
// Simple readiness check using ping
async function dbReady() {
try {
await db.command({ ping: 1 });
return true;
} catch (e) {
return false;
}
}
const app = express();
app.get('/healthz', async (req, res) => {
res.json({ ok: await dbReady() });
});
app.get('/smoke', async (req, res) => {
const col = db.collection('items');
await col.insertOne({ t: new Date(), v: Math.random() });
const count = await col.countDocuments();
res.json({ count });
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`HTTP on ${port}`));
}
start().catch((e) => {
console.error('Fatal:', e);
process.exit(1);
});
Keep configuration in environment variables. Practice a short outage by restarting primaries in staging and watch the app recover.
6) Execution plan
Choose the right flow for your topology.
Single node
- Stop writes from the application (set maintenance mode or stop service)
- Backup: take a snapshot and a mongodump
- Stop mongod
- Install or switch to the new MongoDB binary or container image
- Start mongod and watch logs for successful startup
- Run basic checks and smoke tests
- If all good, enable writes
Replica set (rolling, near zero downtime)
- Ensure backups and recent snapshots for all nodes
- Upgrade secondaries one by one:
- Stop a secondary
- Install or switch to new binary/image
- Start it and wait until it is SECONDARY and caught up
- Step down the primary from mongosh:
rs.stepDown(60)
- Upgrade the former primary using the same steps
- When all nodes run the new version, set feature compatibility to the new version from the primary:
db.adminCommand({ setFeatureCompatibilityVersion: "X.Y" })
- Run smoke tests and deeper validations
Notes:
- Keep the application connected with retryWrites and sensible timeouts
- Expect brief failovers; the driver should reconnect automatically
Sharded cluster (high level)
- Upgrade config servers first, then mongos routers, then shard replica sets
- Treat each shard as its own rolling upgrade
7) Validation checklist
Run quick checks immediately, then deeper checks within the same window.
Immediate:
- Connect and ping
- Read and write a small document
- Confirm replica set is healthy: PRIMARY and SECONDARY roles as expected
rs.status()
- Confirm expected version:
db.version()
db.serverBuildInfo().version
Deeper:
- Run slow query and error rate comparisons vs baseline
- Validate critical indexes exist and are used by your hot paths
// Sample index creation during migration
const col = db.getSiblingDB('appdb').items;
col.createIndex({ userId: 1, createdAt: -1 }, { name: 'ix_user_createdAt' })
- Run application-specific smoke tests (login, order create, etc.)
8) Rollback plan
Have a clear, rehearsed path to the previous state.
- For single node: stop writes, stop mongod, restore from snapshot or mongorestore to a fresh data directory, start the previous version binary/image, run validation
- For replica sets: roll nodes back one by one to the previous binary/image, restoring data only if the node cannot start cleanly
- If you changed feature compatibility, avoid using new features until the validation window ends, so rollback stays simple
- Keep the app capable of connecting to the previous cluster version without code changes
Rollback smoke list:
- App connects and passes /smoke
- Data shape and key indexes match expectations
- Primary election and replication are healthy
9) Monitoring and security
Monitoring:
- Track connections, operation latencies, replication lag, and CPU/memory
- Tail logs for warnings and slow queries
Security:
- Require authentication and least-privilege roles for the app user
- Store connection strings in secrets, not in code
- Use TLS where appropriate and test certificate rotation in staging
Local Pilot Plan
Keep the first pilot small, fast, and easy to debug. The goal is to exercise the critical steps end to end on your laptop.
Scope:
- Single-node MongoDB
- Seed a tiny dataset
- Upgrade to a new version
- Validate with a Node.js Express app
- Practice rollback
1) Docker Compose for a single node
docker-compose.yml:
version: '3.8'
services:
mongo:
image: mongo:6.0
container_name: mongo-old
ports:
- '27017:27017'
volumes:
- ./data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
Start it:
docker compose up -d
Initialize a database user for the app (mongosh inside the container or from your host):
mongosh "mongodb://root: example@localhost:27017/admin" --eval 'db.getSiblingDB("appdb").createUser({ user: "app", pwd: "appsecret", roles: [ { role: "readWrite", db: "appdb" } ] })'
2) Seed data
export MONGODB_URI="mongodb://app: appsecret@localhost:27017/appdb"
mongosh "$MONGODB_URI" --eval 'db.items.insertMany([{name:"alpha"},{name:"beta"}])'
3) Run the Express app
Create app.js from the earlier snippet and run:
export MONGODB_URI="mongodb://app: appsecret@localhost:27017/appdb"
node app.js
Check health and smoke:
curl http://localhost:3000/healthz
curl http://localhost:3000/smoke
4) Backup
mongodump --uri "$MONGODB_URI" --out ./backup-preupgrade
Optionally take a filesystem snapshot of ./data.
5) Upgrade locally
Stop the old container and start a new one with the same data directory:
docker compose down
Replace the image tag to a newer version in docker-compose.yml, for example:
image: mongo:7.0
Start it:
docker compose up -d
Watch logs until you see a clean startup:
docker logs -f mongo-old
Run validation:
mongosh "$MONGODB_URI" --eval 'db.version()'
curl http://localhost:3000/healthz
curl http://localhost:3000/smoke
Optionally set feature compatibility if needed:
mongosh "mongodb://root: example@localhost:27017/admin" --eval 'db.adminCommand({ setFeatureCompatibilityVersion: "X.Y" })'
6) Simple migration example
Create an index and backfill a field:
mongosh "$MONGODB_URI" --eval '
db.items.createIndex({ name: 1 }, { name: "ix_name" });
db.items.updateMany({ migrated: { $exists: false } }, { $set: { migrated: true } });
'
Verify:
mongosh "$MONGODB_URI" --eval 'db.items.getIndexes()'
7) Rollback drill
Stop the new container, restore the dump into a fresh data dir, and run the previous image.
# Stop and remove containers
docker compose down
# Move current data aside and recreate a clean directory
mv ./data ./data-new && mkdir ./data
# Restore backup into a standalone mongod
mongod --dbpath ./data --bind_ip 127.0.0.1 --port 27018 --fork --logpath ./mongod.log
mongorestore --uri "mongodb://localhost:27018/appdb" ./backup-preupgrade/appdb
# Stop the temporary mongod
mongosh "mongodb://localhost:27018/admin" --eval 'db.shutdownServer()'
# Switch image back to mongo:6.0 in docker-compose.yml and start
docker compose up -d
# Recheck app smoke tests
curl http://localhost:3000/healthz
curl http://localhost:3000/smoke
Outcome: you can upgrade, validate, and roll back locally within minutes.
Conclusion
You now have a practical path to upgrade MongoDB with low risk:
- Separate planning, backups, staging, execution, validation, and rollback
- Use a rolling approach for replica sets to minimize downtime
- Validate with targeted health checks, Node.js smoke tests, and index verification
- Keep a tested rollback using snapshots and mongodump
- Bake in monitoring and least-privilege security from the start
Next steps:
- Expand the local pilot into a staging replica set and repeat the drills
- Automate smoke tests and checks in your deployment scripts
- Document your exact cutover and rollback commands for production