MongoDB with Docker Compose: Authentication, Replica Set, and Production-Ready Setup

mongodb docker compose

Table of Contents

  1. Why a Standalone Container Isn’t Enough
  2. Basic Setup: Single Container with Persistence
  3. Enabling Authentication
  4. Single-Node Replica Set for Transactions
  5. Keyfile Authentication for Replica Sets
  6. Full 3-Node Replica Set for High Availability
  7. Health Checks and Auto-Initialization
  8. Connection String Patterns
  9. Backup Considerations
  10. Common Issues and Quick Fixes
  11. Closing Notes

MongoDB is straightforward to start in Docker — pull the image, mount a volume, and you have a working database in under a minute. The complexity shows up the moment your application needs something a single standalone instance can’t provide: multi-document transactions, change streams, or basic high availability. All three require a replica set, and setting one up correctly inside Docker Compose has a few sharp edges worth knowing before you hit them in production.

Why a Standalone Container Isn’t Enough

A default MongoDB container — just image: mongo with no extra flags — runs as a standalone instance. That’s fine for simple CRUD prototyping, but it silently blocks two things many applications eventually need:

  • Multi-document transactions — MongoDB only supports ACID transactions across a replica set, even a single-node one.
  • Change streams — real-time data pipelines, cache invalidation, or event-driven architectures built on watch() require replication to function at all.

If you’ve ever seen an ORM or driver (Prisma is a common example) throw an error demanding a replica set, this is why. The fix isn’t complicated, but it does mean a standalone container is rarely the right default for anything beyond a quick prototype.

Basic Setup: Single Container with Persistence

Start with the foundation — a container that keeps its data after a restart:

version: '3.8'
services:
  mongo:
    image: mongo:7.0
    container_name: mongodb
    restart: always
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:
    driver: local

The official MongoDB image stores its database files at /data/db. As with Redis, a named volume is preferable to a bind mount here — Docker manages its lifecycle, and it stays portable across hosts.

Enabling Authentication

Authentication is not enabled by default on the official MongoDB image. The first time you run it, set a root user through environment variables:

services:
  mongo:
    image: mongo:7.0
    container_name: mongodb
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:
    driver: local

These variables only take effect on first initialization of an empty data directory — changing them later won’t update an existing user. Keep the password in a .env file excluded from version control rather than hardcoded in the compose file.

Connect to verify:

mongosh --port 27017 --username root --password --authenticationDatabase admin

Single-Node Replica Set for Transactions

The fastest way to unlock transactions and change streams in development is converting your single container into a one-member replica set — no extra containers required:

services:
  mongo:
    image: mongo:7.0
    container_name: mongodb
    restart: always
    command: ["--replSet", "rs0", "--bind_ip_all"]
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: >
        echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }"
        | mongosh --port 27017 -u root -p ${MONGO_ROOT_PASSWORD} --authenticationDatabase admin --quiet
      interval: 5s
      timeout: 15s
      start_period: 15s
      retries: 10

volumes:
  mongo-data:
    driver: local

The health check does double duty here: it both verifies MongoDB is responsive and initializes the replica set automatically on first boot if it hasn’t been configured yet. This is a convenient pattern for local development and staging — for production, see the dedicated initialization service further down.

Keyfile Authentication for Replica Sets

Here’s the edge that catches people off guard: once you combine authentication with replication, MongoDB requires internal cluster members to authenticate with each other using a shared keyfile — separate from the root user’s password. Without it, replica set members can’t establish trust between themselves.

Generate a keyfile and mount it:

openssl rand -base64 756 > mongo-keyfile
chmod 400 mongo-keyfile
services:
  mongo:
    image: mongo:7.0
    command: ["--replSet", "rs0", "--keyFile", "/etc/mongo-keyfile", "--bind_ip_all"]
    volumes:
      - mongo-data:/data/db
      - ./mongo-keyfile:/etc/mongo-keyfile:ro
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}

A common permissions trap: the official image runs as the mongodb user internally, and a keyfile mounted with the wrong ownership or overly permissive mode (anything looser than 400) will be silently rejected at startup with a generic authentication error. If replication fails to establish after adding a keyfile, check permissions first.

Full 3-Node Replica Set for High Availability

For an environment that actually needs failover — not just transaction support — run three MongoDB instances on a shared network:

version: '3.8'
services:
  mongo1:
    image: mongo:7.0
    container_name: mongo1
    hostname: mongo1
    command: ["mongod", "--replSet", "rs0", "--keyFile", "/etc/mongo-keyfile", "--bind_ip_all"]
    volumes:
      - mongo1-data:/data/db
      - ./mongo-keyfile:/etc/mongo-keyfile:ro
    networks:
      - mongo-net
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping').ok", "--quiet"]
      interval: 10s
      timeout: 5s
      retries: 5

  mongo2:
    image: mongo:7.0
    container_name: mongo2
    hostname: mongo2
    command: ["mongod", "--replSet", "rs0", "--keyFile", "/etc/mongo-keyfile", "--bind_ip_all"]
    volumes:
      - mongo2-data:/data/db
      - ./mongo-keyfile:/etc/mongo-keyfile:ro
    networks:
      - mongo-net

  mongo3:
    image: mongo:7.0
    container_name: mongo3
    hostname: mongo3
    command: ["mongod", "--replSet", "rs0", "--keyFile", "/etc/mongo-keyfile", "--bind_ip_all"]
    volumes:
      - mongo3-data:/data/db
      - ./mongo-keyfile:/etc/mongo-keyfile:ro
    networks:
      - mongo-net

  mongo-init:
    image: mongo:7.0
    depends_on:
      mongo1:
        condition: service_healthy
    networks:
      - mongo-net
    entrypoint: >
      bash -c "
        mongosh --host mongo1 -u root -p ${MONGO_ROOT_PASSWORD} --authenticationDatabase admin --eval '
        rs.initiate({
          _id: \"rs0\",
          members: [
            { _id: 0, host: \"mongo1:27017\" },
            { _id: 1, host: \"mongo2:27017\" },
            { _id: 2, host: \"mongo3:27017\" }
          ]
        })'
      "

volumes:
  mongo1-data:
  mongo2-data:
  mongo3-data:

networks:
  mongo-net:
    driver: bridge

Each node uses its service name as hostname within the mongo-net network — mongo1, mongo2, mongo3 — and the replica set configuration references those names directly rather than IP addresses, which would break the moment containers restart and get reassigned. This is the same hostname-based service discovery pattern used in any multi-container Compose stack, and it’s worth pairing with the network segmentation approach in our Docker Container Security Best Practices guide if this stack is internet-facing in any way.

Health Checks and Auto-Initialization

A dedicated mongo-init service — as shown above — is the cleanest way to handle replica set initialization in a repeatable, automated way, rather than running rs.initiate() manually after every fresh deployment. The pattern:

  1. mongo-init waits for mongo1 to report healthy via depends_on.condition: service_healthy.
  2. It runs rs.initiate() exactly once, targeting all three members.
  3. The container exits after running — it’s a one-shot initialization job, not a long-running service.

If you re-run docker compose up on an already-initialized cluster, rs.initiate() will simply fail harmlessly since the replica set already exists — safe to leave in place for idempotent deployments.

Connection String Patterns

Once authentication and replication are both in place, your application’s connection string needs two extra parameters compared to a standalone setup:

mongodb://appuser:apppassword@mongo1:27017,mongo2:27017,mongo3:27017/myapp?replicaSet=rs0&authSource=admin
  • Multiple hosts in the connection string let the driver discover the current primary even after a failover.
  • replicaSet=rs0 must match the _id used in rs.initiate().
  • authSource=admin tells the driver which database the user’s credentials are stored in — almost always admin for a root-style user, even when connecting to a different application database.

For local development against the single-node setup, the equivalent is simpler:

mongodb://root:password@localhost:27017/?replicaSet=rs0&authSource=admin

Backup Considerations

A replica set doesn’t replace backups — replication protects against node failure, not against an accidental deleteMany() or application bug that corrupts data across all members simultaneously. The backup approach is the same regardless of whether you’re running standalone or a replica set: mongodump against a secondary node to avoid load on the primary, archived on a schedule outside the container. Our existing MongoDB Backup guide covers the full scripted approach, including restore testing — worth pairing with this setup once your replica set is running.

If your stack also runs Redis alongside MongoDB, the volume-and-backup discipline described in our Redis with Docker Compose guide follows the same underlying pattern — named volumes, scheduled snapshots, and a tested restore procedure for each.

Common Issues and Quick Fixes

SymptomLikely CauseFix
Driver throws “Transaction numbers are only allowed on a replica set”Standalone instance with no replication configuredConvert the deployment to a single-node or multi-node replica set.
Replica set members can’t authenticate with each otherMissing or incorrectly configured keyfileGenerate a keyfile, mount it correctly, set permissions to chmod 400, and verify file ownership.
rs.initiate() hangs or times outReplica set members cannot resolve each other’s hostnamesEnsure all nodes are connected to the same Docker network and use service names instead of localhost.
Application can’t find the primary node after failoverConnection string references only one hostInclude all replica set members in the MongoDB connection string.
MONGO_INITDB_ROOT_USERNAME has no effectEnvironment variables only apply during first initialization of an empty volumeDelete and recreate the volume, or manually create the user using mongosh.

Conclusion

Running MongoDB in Docker Compose is simple for a prototype and only slightly more involved once production requirements show up: authentication from the start, a replica set the moment transactions or change streams enter the picture, a keyfile the moment authentication and replication combine, and health checks that make initialization repeatable instead of a manual one-off step.

From here, pair this setup with our MongoDB Backup guide for the operational side, and our Redis with Docker Compose guide if your stack runs both databases side by side — the persistence and security patterns carry over directly between the two.

(Visited 3 times, 3 visits today)

You may also like