Here's an uncomfortable question for anyone running MongoDB in production: when did you last restore one of your backups?
Not "check that the cron job ran." Not "confirm the dump landed in the bucket." Actually restore it, into a real database, and query the data.
If the answer is "never," you don't have backups. You have a folder of files you hope are backups. Restore failures are almost never discovered during backups; they're discovered during outages, which is the one moment you can't afford them.
MongoDB adds two of its own traps. First, mongodump and mongorestore ship as MongoDB Database Tools, a separate download from the server, so the machine running your cron job may not even have them until you install them. Second, on a busy replica set a plain mongodump reads collections one at a time and can capture them at slightly different moments, giving you a dump that's internally inconsistent. Both restore without error and look fine, right up until they don't.
This guide builds a MongoDB backup pipeline where consistency and restore are part of the pipeline, not an afterthought:
- Dump to a single consistent archive
- Verify the archive is readable
- Prove it restores (the step everyone skips)
- Ship it off-site: encrypted, deduplicated, versioned
- Apply retention automatically
- Put the whole thing on a schedule
Total time: about 10 minutes. Total cost: free up to 5 GB.
1. Dump to a single consistent archive
First, make sure the Database Tools are actually installed (they are not part of the MongoDB server package):
- macOS:
brew install mongodb-database-tools - Debian/Ubuntu: install the MongoDB Database Tools package
The cleanest output for backup is one gzipped archive file, which fits the "one stable file" pattern perfectly:
mkdir -p ./db-dumps
mongodump \
--uri="mongodb://user:[email protected]:27017/app_db" \
--archive=./db-dumps/app.archive \
--gzip
What each flag buys you:
| Flag | Why it matters |
|---|---|
--uri | Full connection string: host, port, credentials, database. Omit the database to dump the whole server. |
--archive=<file> | Writes a single archive file instead of a directory tree of BSON. One file is far easier to version and upload. |
--gzip | Compresses the archive. |
Two habits worth building in now:
- Keep the file path stable. Overwrite
./db-dumps/app.archiveon every run. That stable path is what makes deduplicated, incremental uploads effective later: only the changed blocks ship. - Keep credentials out of shell history. Prefer environment variables or a config file over inlining the password in the URI where
psand your history file can see it.
On a replica set, add --oplog. This captures a slice of the oplog alongside the dump so mongorestore --oplogReplay can bring everything to a single consistent point in time. Without it, a busy cluster gives you collections snapshotted at slightly different moments:
mongodump \
--uri="mongodb://user:[email protected]:27017/?replicaSet=rs0" \
--archive=./db-dumps/app.archive \
--gzip \
--oplog
Dump the whole server by omitting the database from the URI:
mongodump --uri="mongodb://user:[email protected]:27017" \
--archive=./db-dumps/all.archive --gzip
2. Verify the archive is readable
Thirty seconds, and it catches the dumb failures: a truncated archive, or a dump that died mid-write. You can inspect an archive's contents without restoring anything, using a dry run:
ls -lh ./db-dumps/app.archive
mongorestore --archive=./db-dumps/app.archive --gzip --dryRun --verbose 2>&1 | head
A readable table of collections means the archive header and structure are intact. That's a plausible backup. Plausible isn't proven.
3. Prove it restores
This is the step that separates a backup strategy from a folder of files. Restore into a different namespace so you never touch the original data. The --nsFrom/--nsTo remapping rewrites app_db.* to app_db_restore.* on the way in:
mongorestore \
--uri="mongodb://user:[email protected]:27017" \
--archive=./db-dumps/app.archive \
--gzip \
--nsFrom='app_db.*' --nsTo='app_db_restore.*'
# Then look at the data. Document counts, latest records, whatever proves it's real:
mongosh "mongodb://user:[email protected]:27017/app_db_restore" \
--eval "db.users.countDocuments()"
Time this step with a stopwatch. That wall-clock number is your real RTO, the minimum downtime you'd eat in a genuine recovery. mongorestore rebuilds indexes as part of the restore, which on large collections can dominate the clock, so the number is often larger than people expect. Better to learn it now than during an incident.
If you dumped with --oplog, add --oplogReplay to the restore to apply the captured oplog and land on that single consistent point in time.
4. Ship it off-site
A verified archive sitting next to the database dies with the database. The classic 3-2-1 rule: three copies, two media, one off-site.
You could aws s3 cp the archive into a bucket, but a bucket is storage, not backup logic. No versioned snapshots, no integrity verification on restore, no retention policy, no deduplication (you pay full price to store 30 nearly-identical archives), and encryption is on you.
This is what Backup Data handles. Sign in, claim the free 5 GB workspace, mint an API key scoped backup:write, backup:read, snapshots:read, then:
npm install @lighthouse-web3/baas-js-sdk
export LH_API_KEY="lh_xxxxxxxxxxxxxxxxxxxxxxxx"
export LH_WORKSPACE_ID="your-workspace-uuid"
import { BackupClient } from "@lighthouse-web3/baas-js-sdk";
const client = new BackupClient({
apiKey: process.env.LH_API_KEY,
workspaceId: process.env.LH_WORKSPACE_ID,
});
const snapshot = await client.backup(["./db-dumps"], {
description: "nightly database backup",
tags: { type: "db-backup", env: "prod", db: "app_db" },
});
console.log(`snapshotId=${snapshot.snapshotId} totalSize=${snapshot.totalSize}`);
(There's a Go SDK with the same surface if that's your stack.)
Each run creates an immutable, point-in-time snapshot. Because uploads are content-addressed and chunked (FastCDC), a nightly archive that's 2% different from yesterday's uploads roughly 2% of the bytes. One honest caveat specific to MongoDB: the dump is already gzipped, so BaaS dedupes at the chunk level rather than re-compressing, and a small change early in a compressed stream can shift bytes downstream. You'll still save meaningfully on a slowly-changing database; check your real ratio in the portal after two uploads rather than assuming best case.
If the dump contains anything sensitive (it does), enable client-side encryption. Data is AES-GCM-encrypted on your machine before upload; the server only ever stores ciphertext:
import { generateKeyfile } from "@lighthouse-web3/baas-js-sdk";
// once:
generateKeyfile("/secure/lh.keyfile", process.env.LH_KEYFILE_PASSPHRASE);
// then on every backup:
const encrypted = await client.backup(["./db-dumps"], {
description: "nightly-encrypted",
encryption: {
keyfilePath: "/secure/lh.keyfile",
passphrase: process.env.LH_KEYFILE_PASSPHRASE,
},
});
One warning that deserves bold: you hold the keys. Lose the keyfile and passphrase and the data is unrecoverable. There is no server-side reset. Store the keyfile like you store any root credential.
5. Retention, so storage doesn't grow forever
Nightly snapshots for two years is 700+ snapshots you'll never use. Encode a retention policy, and dry-run it before letting it delete anything:
const keepLatest = 14;
const before = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
// Preview: what would be deleted?
const preview = await client.pruneSnapshots({ keepLatest, before, dryRun: true });
console.log("would delete:", preview.snapshotIds);
// Then for real:
await client.pruneSnapshots({ keepLatest, before, dryRun: false });
A snapshot is deleted only when it's both outside the newest 14 and older than 30 days. Chunks still referenced by surviving snapshots are kept automatically; deleting a snapshot never corrupts its neighbors.
6. Schedule it, including the restore drill
Wire dump + upload into cron or CI (mint a dedicated API key for the job):
# /etc/cron.d/mongodb-backup (nightly at 02:15)
15 2 * * * backup /opt/backup/run-backup.sh >> /var/log/mongo-backup.log 2>&1
Where run-backup.sh is the mongodump command from step 1 followed by node upload.mjs with the backup() call from step 4.
And schedule the drill, because step 3 wasn't a one-time ceremony. Monthly, restore the latest snapshot into a scratch directory and load it into a throwaway namespace:
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const target = mkdtempSync(join(tmpdir(), "lh-restore-drill-"));
await client.restore(snapshot.snapshotId, target, {
onProgress: (e) => console.log(`[${e.phase}] ${e.current}/${e.total}`),
});
// then: mongorestore --archive=${target}/db-dumps/app.archive --gzip \
// --nsFrom='app_db.*' --nsTo='app_db_restore.*'
Every restore verifies chunk integrity against expected checksums before reassembly. A silently corrupted backup fails loudly at drill time, not at incident time.
The drill checklist:
- ✅ Restore into a fresh namespace. Never over live data
- ✅ Leave checksum verification on
- ✅ Confirm the
--dryRuncollection list matches what you expect - ✅ Spot-check document counts on your key collections
- ✅ Record wall-clock time, including index rebuilds (that's your RTO, keep a log of it)
- ✅ Drop the scratch namespace and directory when done
The pipeline, complete
| Step | Command / call | Proves |
|---|---|---|
| Dump | mongodump --archive --gzip (--oplog on replica sets) | You have consistent data |
| Verify | mongorestore --dryRun | The archive is readable |
| Restore drill | mongorestore into a scratch namespace | It actually restores |
| Off-site | client.backup([...]) | It survives losing the box |
| Retention | client.pruneSnapshots({...}) | Storage stays bounded |
| Schedule | cron + monthly drill | It happens without you |
The difference between this and the mongodump | aws s3 cp script in most repos isn't the dump. It's that consistency, restore, verification, encryption, dedupe, and retention are part of the loop instead of TODOs.
Start with the free tier: backupdata.io has 5 GB free, no card, and the 10-minute quickstart gets you to a first verified snapshot today.