Blog · Copying data

How to copy a MongoDB database to another server

Friday, 4 pm: “just copy the database to staging”. The first answer on Google still says db.copyDatabase(). It was removed in MongoDB 4.2, back in 2019. Here is what works today, for a whole database or a single collection, and the six things that bite everyone on the first try.

Short answer

Pipe mongodump straight into mongorestore. No temp files, no disk:

mongodump --uri="mongodb://source-host:27017" --db=shop --archive \
  | mongorestore --uri="mongodb://target-host:27017" --archive

No mongodump on your machine? Since MongoDB 4.4 the Database Tools are a separate download.

Pick the tool

Same server $out / $merge The server copies it by itself. Fast, but it can't leave the cluster, and $out doesn't bring the indexes.
Another server, once mongodump | mongorestore The standard. Works everywhere. No resume, no undo.
Another server, often Ctrl+C, Ctrl+V In a GUI like BsonJet: survives a dropped VPN, asks before it overwrites. See below.

The one-liner, two more ways

# the whole database under a new name
mongodump --uri="mongodb://source-host:27017" --db=shop --archive \
  | mongorestore --uri="mongodb://target-host:27017" --archive \
      --nsFrom="shop.*" --nsTo="shop_copy.*"

# just one collection
mongodump --uri="mongodb://source-host:27017" --db=shop --collection=orders --archive \
  | mongorestore --uri="mongodb://target-host:27017" --archive

The six traps

  1. Authentication failed, and the password is right

    Your user lives in admin, but --db=shop makes mongodump log in against shop. A database in the URI path (…:27017/shop) does the same. All you get is (AuthenticationFailed) Authentication failed.

    add authSource=admin to the source URI:

    mongodump --uri="mongodb://user:pass@source-host:27017/?authSource=admin" --db=shop ...
  2. Restoring into an existing collection doesn't overwrite anything

    mongorestore only inserts. A document whose _id is already there fails as a duplicate, the old version stays, and at the very end you get 1520 document(s) failed to restore. Congratulations, you have a half-updated collection.

    add --drop. It drops each collection before restoring it. Check the target URI twice; there is no undo.

  3. Stuck at 100% for twenty minutes

    It isn't frozen. The data is in; now the target builds the indexes (after the data, one collection at a time). On a big collection that is the long part. Don't Ctrl+C it.

    // on the target: see the index builds working
    db.currentOp({ "command.createIndexes": { $exists: true } })
  4. The VPN drops at 87%

    mongorestore can't continue. Run it again with --drop and you are back at 0%. Without it, it skips what is already there, slowly: the whole dump goes over the wire again and every existing document is reported as failed (see trap 2).

    for big data over a shaky link, dump to a file on a machine next to the source, move the file with something that resumes (rsync --partial), restore next to the target.

  5. The copy works, the app can't log in

    mongodump --db takes your collections, not the people allowed to read them. Users usually live in admin.

    you need both: --dumpDbUsersAndRoles on dump and --restoreDbUsersAndRoles on restore. And only for users created in that database. Users created in admin (the usual way) you create on the target yourself.

  6. You just slowed down production

    By default mongodump reads everything through the primary, the same one serving your users right now.

    add --readPreference=secondary to mongodump, with the replica set URI (all members or mongodb+srv://). Pointed at a single host, it reads from that host.

Did it really copy everything?

Matching counts prove exactly one thing: the counts match.

// run on both servers
db.orders.countDocuments()
db.orders.aggregate([{ $group: { _id: null, sum: { $sum: "$total" } } }])

A sum of a numeric field catches more than a count, and costs one collection scan. A real field-by-field check needs a compare tool. Which brings us to:

The Ctrl+C, Ctrl+V way

We got tired of all of the above, so BsonJet copies data like files: select a database or a few collections in the tree, Ctrl+C, click the target server, Ctrl+V. Or right-click → Copy collection to… for a new name or only part of the data.

BsonJet Copy collection dialog: target connection, database and collection, a filter, what to do if the target exists, and Copy indexes
New name, only the documents you filter, and a choice for when the target already exists.
  • Trap 1 never happens. The copy uses the connection you already browse with: if you can see the data, you can copy it. No pipes, no temp files, raw BSON, several collections in parallel, compressed on the wire.
  • Trap 2, solved by asking. If the target collection exists, you choose: drop and replace, update documents with the same _id, add only new ones, or skip.
  • Trap 4, solved by waiting. The connection drops, the job waits up to 10 minutes and continues from the last _id it copied. Nothing twice, nothing from zero.
  • Trap 5, honestly: users and roles aren't copied either. Create them on the target.

And because Friday at 4 pm is when it happens:

BsonJet confirmation: copy from staging to production, the target is framed in red with the warning that it looks like production
“prod” anywhere in the target and the confirmation turns red. Enter doesn't confirm it: Cancel has the focus.

Afterwards, Compare database with… checks both sides field by field and syncs what differs. That's the check counts can't do.

Try it on your own data.

The full version is free for personal use, no registration.

Download for Windows, macOS or Linux