
Running MongoDB in Docker and Docker Compose
Installing MongoDB directly on your laptop works, until you need a different version for another project, or a teammate's setup behaves differently from yours, or you want a clean database for a test run. Docker solves all of that. A MongoDB container starts in seconds, runs the exact version you specify, and disappears without a trace when you're done.
But the one-liner most tutorials show, docker run mongo, leaves out almost everything that matters: your data vanishes when the container is removed, there's no authentication, and it's a standalone server, so transactions and change streams won't work. The gap between "a MongoDB container" and "a MongoDB container you can build an app against" is a handful of settings you need to get right.
This guide covers running MongoDB with docker run, persisting data with volumes, enabling authentication, seeding data with init scripts, defining everything in Docker Compose, running a replica set (single-node and three-node), connecting from your app, and backing up containerized data.
Choosing an Image
There are two main MongoDB images:
mongo: the Docker Official Image, maintained by the Docker community with MongoDB's involvement. It's the most widely used and supports the handyMONGO_INITDB_*environment variables and init scripts.mongodb/mongodb-community-server: published by MongoDB itself, based on Red Hat UBI.
Both work well. This guide uses the official mongo image because its initialization features are the most convenient for development.
Always pin a version tag. mongo:latest changes meaning when a new major version is released, which can break your setup (and your data files) without warning. Use at least the major version, like mongo:8.0, and consider pinning the exact patch for production-like environments.
The Quick Start
docker run -d --name mongo -p 27017:27017 mongo:8.0
Connect with mongosh from inside the container:
docker exec -it mongo mongosh
Current Mongosh Log ID: 66f5...
Connecting to: mongodb://127.0.0.1:27017/?directConnection=true
Using MongoDB: 8.0.x
Using Mongosh: 2.x.x
test>
That's a working server, but remove the container and your data goes with it. Let's fix that.
Persisting Data with Volumes
MongoDB stores data in /data/db inside the container. Mount a named volume there:
docker volume create mongo-data
docker run -d --name mongo \
-p 27017:27017 \
-v mongo-data:/data/db \
mongo:8.0
Now you can stop, remove, and recreate the container, and the data survives as long as the volume exists.
Named volumes are generally better than bind mounts (like -v ./data:/data/db) for database files. On macOS and Windows, bind-mounted directories go through a file-sharing layer that is much slower and has occasionally caused problems with WiredTiger's file operations. Named volumes live inside the Docker VM's filesystem and avoid that.
Enabling Authentication
The official image enables authentication and creates a root user if you set two environment variables:
docker run -d --name mongo \
-p 27017:27017 \
-v mongo-data:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=change-me \
mongo:8.0
Connect with credentials:
docker exec -it mongo mongosh -u admin -p change-me --authenticationDatabase admin
An important detail: these variables only take effect when the data directory is empty, on the very first startup. If you started the container once without them, the volume already has data and the root user won't be created. Remove the volume (docker volume rm mongo-data) and start again, or create the user manually.
To keep the password out of your shell history and docker inspect output, the image also supports _FILE variants that read from a file, which pairs well with Docker secrets:
-e MONGO_INITDB_ROOT_PASSWORD_FILE=/run/secrets/mongo_root_password
Seeding Data with Init Scripts
Any .js or .sh file mounted into /docker-entrypoint-initdb.d/ runs on first initialization, in alphabetical order. .js files run with mongosh against the database named in MONGO_INITDB_DATABASE (or test if it's unset).
Create an app user with limited privileges and some seed data:
// docker/mongo-init/01-app-user.js
const appDb = db.getSiblingDB("shop");
appDb.createUser({
user: "shop_app",
pwd: process.env.SHOP_APP_PASSWORD,
roles: [{ role: "readWrite", db: "shop" }],
});
appDb.products.createIndex({ sku: 1 }, { unique: true });
appDb.products.insertMany([
{ sku: "LAMP-001", name: "Brass Desk Lamp", price: NumberDecimal("89.00") },
{
sku: "CHAIR-014",
name: "Oak Dining Chair",
price: NumberDecimal("189.00"),
},
]);
print("Seeded shop database");
mongosh scripts can read environment variables through process.env, so pass SHOP_APP_PASSWORD to the container as well. Like the root user variables, init scripts only run when the data directory is empty. They won't re-run on restart, which is exactly what you want for seed data. For larger fixture setups, see Data Seeding and Fixtures for MongoDB Development Environments.
Docker Compose: The Development Setup
Typing long docker run commands gets old. Here's the same setup in Compose, along with a healthcheck so dependent services wait until MongoDB is actually ready:
# docker-compose.yml
services:
mongo:
image: mongo:8.0
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: shop
SHOP_APP_PASSWORD: ${SHOP_APP_PASSWORD}
volumes:
- mongo-data:/data/db
- ./docker/mongo-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test:
["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
api:
build: .
environment:
MONGODB_URI: mongodb://shop_app:${SHOP_APP_PASSWORD}@mongo:27017/shop?authSource=shop
depends_on:
mongo:
condition: service_healthy
ports:
- "3000:3000"
volumes:
mongo-data:
Put the passwords in a .env file next to the Compose file (and add it to .gitignore):
# .env
MONGO_ROOT_PASSWORD=local-root-pass
SHOP_APP_PASSWORD=local-app-pass
Notice the connection string for the api service uses the hostname mongo, the service name. Inside a Compose network, services find each other by name. From your host machine, you'd connect to localhost:27017 instead. The authSource=shop parameter is needed because the app user was created in the shop database.
Start it:
docker compose up -d
docker compose ps
NAME SERVICE STATUS PORTS
shop-mongo-1 mongo Up 25 seconds (healthy) 0.0.0.0:27017->27017/tcp
shop-api-1 api Up 3 seconds 0.0.0.0:3000->3000/tcp
Running a Replica Set
A standalone server is fine for basic CRUD, but transactions and change streams require a replica set. Many developers hit this the first time they call session.withTransaction() against a local container and get an error. The fix is a single-node replica set: one member, but running in replica set mode.
Single-Node Replica Set
The trick is starting mongod with --replSet and initiating the set once. A healthcheck can do the initiation automatically:
services:
mongo:
image: mongo:8.0
command: ["--replSet", "rs0", "--bind_ip_all", "--port", "27017"]
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
healthcheck:
test: >
mongosh --port 27017 --quiet --eval "
try { rs.status().ok }
catch (e) { rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'localhost:27017' }] }).ok }
"
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
volumes:
mongo-data:
On the first healthcheck, rs.status() throws because the set isn't initialized, so the catch branch runs rs.initiate(). After that, rs.status() succeeds and the check passes.
The member host is set to localhost:27017, which makes it easy to connect from your host machine. Connect with directConnection=true so the driver doesn't try to discover other members:
mongosh "mongodb://localhost:27017/?directConnection=true"
If other containers need to connect too, localhost won't resolve to the MongoDB container for them. In that case, set the member host to the service name (mongo:27017) and, from your host machine, either add 127.0.0.1 mongo to your hosts file or keep using directConnection=true.
Replica Sets with Authentication
When you enable authentication on a replica set, members also need a keyfile to authenticate to each other, even if there's only one member. Generate one and fix its permissions, since mongod refuses keyfiles that are readable by others:
openssl rand -base64 756 > docker/mongo-keyfile
chmod 400 docker/mongo-keyfile
Then pass --keyFile in the command. Inside the container, the file must be owned by the mongodb user (UID 999 in the official image), which is often easiest to handle with a small entrypoint wrapper that copies the key and runs chown before starting mongod. For local development, many teams skip auth on the single-node replica set entirely and keep it on the network-isolated Compose stack.
Three-Node Replica Set
To test failover behavior locally, run three members:
services:
mongo1:
image: mongo:8.0
command: ["--replSet", "rs0", "--bind_ip_all"]
volumes: ["mongo1-data:/data/db"]
ports: ["27017:27017"]
healthcheck:
test: >
mongosh --quiet --eval "
try { rs.status().ok }
catch (e) { rs.initiate({ _id: 'rs0', members: [
{ _id: 0, host: 'mongo1:27017', priority: 2 },
{ _id: 1, host: 'mongo2:27017' },
{ _id: 2, host: 'mongo3:27017' }
] }).ok }
"
interval: 5s
retries: 20
start_period: 10s
depends_on: [mongo2, mongo3]
mongo2:
image: mongo:8.0
command: ["--replSet", "rs0", "--bind_ip_all"]
volumes: ["mongo2-data:/data/db"]
mongo3:
image: mongo:8.0
command: ["--replSet", "rs0", "--bind_ip_all"]
volumes: ["mongo3-data:/data/db"]
volumes:
mongo1-data:
mongo2-data:
mongo3-data:
Test a failover by stopping the primary and watching an election happen:
docker compose stop mongo1
docker compose exec mongo2 mongosh --quiet --eval "rs.status().members.map(m => m.name + ' ' + m.stateStr)"
[ 'mongo1:27017 (not reachable/healthy)', 'mongo2:27017 PRIMARY', 'mongo3:27017 SECONDARY' ]
Application containers on the same Compose network can use the full replica set connection string, mongodb://mongo1:27017,mongo2:27017,mongo3:27017/?replicaSet=rs0. For more on how elections work, see Understanding MongoDB Replica Sets and High Availability.
Resource Limits and Configuration
Set memory limits and let MongoDB detect them. In recent versions, mongod reads the container's cgroup memory limit when sizing the WiredTiger cache. Without a limit, it sizes the cache from the host's total RAM, which on a laptop running several containers can lead to memory pressure:
services:
mongo:
image: mongo:8.0
deploy:
resources:
limits:
memory: 2g
You can also set the cache explicitly with --wiredTigerCacheSizeGB 0.5 in command.
Use a config file for anything non-trivial. Mount a mongod.conf and point mongod at it:
services:
mongo:
image: mongo:8.0
command: ["mongod", "--config", "/etc/mongod.conf"]
volumes:
- ./docker/mongod.conf:/etc/mongod.conf:ro
- mongo-data:/data/db
Backing Up Containerized Data
The Database Tools ship in the official image, so you can dump straight to your host with docker exec:
docker compose exec -T mongo mongodump \
--username admin --password "$MONGO_ROOT_PASSWORD" --authenticationDatabase admin \
--archive --gzip > backup-$(date +%F).archive.gz
And restore from a file on the host:
docker compose exec -T mongo mongorestore \
--username admin --password "$MONGO_ROOT_PASSWORD" --authenticationDatabase admin \
--archive --gzip --drop < backup-2026-09-26.archive.gz
The -T flag disables the pseudo-TTY so binary data streams correctly through stdin and stdout.
Should You Run MongoDB in Docker in Production?
Docker is ideal for development, CI, and testing. Production is possible, but containers don't change the fundamentals: you still need durable storage on fast disks, replica sets across separate hosts, backups, monitoring, and careful upgrades. Running a replica set on a single Docker host gives you three copies of the data and zero protection against that host failing.
For production, most teams choose one of:
- MongoDB Atlas, which removes the operational work entirely.
- Kubernetes with an operator that handles replica set membership, storage, and upgrades; see Deploying MongoDB on Kubernetes with the MongoDB Operator.
- VMs or bare metal with MongoDB installed from packages and managed with configuration tooling.
Common Pitfalls
Using mongo:latest. A new major version can appear under the same tag, and mongod refuses to start on data files whose feature compatibility version is too old for it. Pin versions and upgrade deliberately.
Expecting init variables to work on existing data. MONGO_INITDB_* variables and init scripts only run on an empty data directory. If you changed them later, recreate the volume.
Forgetting the replica set for transactions. A standalone container throws errors for transactions and change streams. Use a single-node replica set in development.
Mismatched hostnames in replica set configs. The hosts in rs.initiate() must be resolvable by every client that connects. If your app can't connect, check whether it can resolve the member hostnames, or use directConnection=true for single-node setups.
Exposing an unauthenticated port. Publishing 27017 on a machine with a public IP and no auth has led to countless data leaks. Enable authentication, and bind the published port to localhost (127.0.0.1:27017:27017) when you don't need outside access.
Conclusion
Running MongoDB in Docker takes a few deliberate choices: pin the image version, persist /data/db in a named volume, enable authentication with the MONGO_INITDB_* variables, seed with init scripts, and use a healthcheck so dependent services wait for readiness. When your app needs transactions or change streams, switch to a single-node replica set, and use three members when you want to test failover.
Your next step: take the Compose file from the development setup section, add your app as a service, and replace your locally installed MongoDB with it. Once docker compose up gives every developer on your team an identical database, you won't want to go back.


