DEV Community

Abdelfattah Radwan
Abdelfattah Radwan

Posted on

Deploy MongoDB Replica Sets with Ease Using Docker Compose

Hello everyone, and welcome.

A while back, I wrote a blog post that showed how to quickly get a MongoDB cluster up and running without too much hassle. Since then, I have learned a lot more about Docker and MongoDB. This post will show you how to do the same thing more easily and streamlined. Let's get started!

First, let's create the required files. Those are:
.env: An environment file that contains our MongoDB root username, password, and replica set key.
keyfile.sh: A bash script which will put our replica set key in a file and set the correct permission for that file.
docker-compose.yml: The Docker compose file in which we define our services.
Our project directory now should look something like this:

mongodb
├── .env
├── docker-compose.yml
└── keyfile.sh
Enter fullscreen mode Exit fullscreen mode

Now, let's go ahead and set up our environment variables. Which are:

  • MONGODB_ROOT_USERNAME: The username of the root MongoDB user.
  • MONGODB_ROOT_PASSWORD: The password of the root MongoDB user.
  • MONGO_REPLICA_SET_KEY: The replica set key, which we will put in the key file later.

I will use "root" for the username, and for the password, I will use the openssl command to generate 32 random bytes in hex format to use as the password. The command is as follows:

openssl rand -hex 32
Enter fullscreen mode Exit fullscreen mode

The output will look something like this: 4c1adead789af1c3bc7a164a6ede1a4398a74b61a9f5b10aa5db5ef19334962c

Some requirements must be met for the replica set key to be accepted by the replica set nodes. You can find them on MongoDB's documentation website here.

To generate the key, we will use the following command:

openssl rand -base64 756
Enter fullscreen mode Exit fullscreen mode

That will generate a long string of base64 characters, which represents our key.

With all of that done, you should have an environment file that looks something like this:

MONGODB_ROOT_USERNAME="root"
MONGODB_ROOT_PASSWORD="e771096f5659c043bf5117b64eae4001e0ac6fd1344e469cc2ceb0fdb4453620"

MONGO_REPLICA_SET_KEY="y5yYpzyuzGYjK8xMsY+rjm7B/PfmFm/DSBoR/FlYAnoug/5Xyj2bHP7h3D5vFJrAOC9hrIfG/nmj1WgBOqxa2m/NX4aSfbu3jVVR4Oq1YG8od6PhY2cSfHoiHGZiw0WannwuQ1TFiU8EF6R0jxksTZJayAi1sPhEwwvEvHccC2gaPHA30Q4biiUazBnh9W2O9xV5oFSnCzvGRkQk9rhZQcf4X4kFR78Xa7wGtX0Qc3PudLgvTTMz5RlYuCY+r17zHFUohtPM4OdADx0SLB/D74NTasRF4BKlsOrjp8rRUcCTD7pAQ8qCtUukwYArCV0/ryb67oTfazGei/G81mkhhpPCBhxgzsNgjUyAXXupiVNnqQZjNQJBfP8iqYM7u1SYE7FLTZG7OXWDtM1iwxk7F3LS2HRfG8BQSGOAbOvegXccVRSKzilFd8gZkirGvI5ZvVfMZ9PNgSk0VGfukYeiWU+j8EJZlnxkBhMHS17fGhmfoC39E2sdMWacG0JcER+UKyWRT9qpKJw8NH07BcuPFMIYfrFt0PXw3RnVmzPIvmVjYQVL6uIoygJs4qnMBPk9r1fYm+5vklz4v2pZI6SsfcrdWsg+8K4CIiVF+J2wdBmM1zSmG3T95iPxmhCfgkmRs9dz7GI2/axA++jox60DGTtsVTxHQca2JB2pkEG+jwZAtSJXMNkjhlFZVfDyp4MNAj4i3awsHD8vW6v79vyBtxTC6aS03+j2y/0JteI+twt3ASNAwVHoERIfwuNd7hFRTDl//n/7KcVJuiyriRXL+X/JCbg+5s0qZmVqnESOVI4Vi0KLL4Lx/Zir7RfhQ2s2woW2gMULJ+6zfIGY+U7XGYJd/N8IP+HKK2fGaQrYbS0xhqcVzdg3lEvMu3ky7WfxJtRnlqt/TarOod88VzcYlmoRAWmlkNMEjL0/xq5rrQ+u0nYOk3Mgc+MS3P/4X19ITtgm6H5yDb1jRDD9H/C4J7+PuzU8uf9G3zuCcUDCW6zz6AW1"
Enter fullscreen mode Exit fullscreen mode

Now, let's create the keyfile.sh script. As I mentioned, it sets up our keyfile with the correct replica set key and correct permissions. Here's how it should be:

#!/bin/bash

# Echo MONGO_REPLICA_SET_KEY to the keyfile.

echo $MONGO_REPLICA_SET_KEY > /data/configdb/keyfile

# Change the permissions of the keyfile.

chmod 400 /data/configdb/keyfile

# Change the ownership of the keyfile.

chown mongodb:mongodb /data/configdb/keyfile
Enter fullscreen mode Exit fullscreen mode

Now that our env and keyfile.sh files are ready, let's move on to our docker-compose.yml file. It will contain three services:

  • primary: Our primary replica set node.
  • replica_1: Our first secondary node in the replica set.
  • replica_2: Our second secondary node in the replica set.
  • init: Will be used to initialise the replica set.

Beginning with our primary service:

  primary:
    image: mongo:latest
    command: mongod --port 27017 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
      - "27017:27017"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
      - MONGO_INITDB_ROOT_USERNAME=root
      - MONGO_INITDB_ROOT_PASSWORD=root
      - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
      - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
      - primary_data_db:/data
      - primary_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27017 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'
      interval: 5s
Enter fullscreen mode Exit fullscreen mode

As you can see, it's a straightforward service definition with a couple of interesting things:
Under volumes, we mount our keyfile.sh file to docker-entrypoint-initdb.d/keyfile.sh, making Docker execute it during the container's initialisation process.
We defined a healthcheck element, which will ping the MongoDB instance running inside the container, in this case, every 5 seconds, to check if it's "healthy" (started and ready) or not.

Moving on to our replica service definitions. Here's the first replica service definition:

  replica_1:
    image: mongo:latest
    command: mongod --port 27018 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
      - "27018:27018"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
      - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
      - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
      - replica_1_data_db:/data
      - replica_1_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27018 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'
Enter fullscreen mode Exit fullscreen mode

And the second:

  replica_2:
    image: mongo:latest
    command: mongod --port 27019 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
 - "27019:27019"
    extra_hosts:
 - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
 - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
 - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
 - replica_2_data_db:/data
 - replica_2_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27019 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'
Enter fullscreen mode Exit fullscreen mode

They are very similar to the primary service definition, except we don't give them a root username and password, only a replica set key.

Finally, let's define our init service:

  init:
    image: mongo:latest
    restart: no
    extra_hosts:
 - "host.docker.internal:host-gateway"
    depends_on:
      primary:
        condition: service_healthy
      replica_1:
        condition: service_healthy
      replica_2:
        condition: service_healthy
    command: >
 mongosh -u root -p root --host host.docker.internal --port 27017 --eval '
 rs.initiate({
 _id: "rs0",
 members: [
 { _id: 0, host: "host.docker.internal:27017" },
 { _id: 1, host: "host.docker.internal:27018" },
 { _id: 2, host: "host.docker.internal:27019" }
 ]
 })
 '
Enter fullscreen mode Exit fullscreen mode

As I mentioned before, this service will initialise the replica set. It will do that by:
Waiting until the primary and all secondary nodes are ready.
Execute a command using mongosh to initialise the replica set.

Unlike the previous version of this post, we no longer need a bash script and a while loop to check if the nodes are ready. We use healthcheck elements and the depends_on element to emulate that behaviour more straightforwardly.

The final docker-compose.yml file should look something like this:

name: mongodb_replica_set

services:
  primary:
    image: mongo:latest
    command: mongod --port 27017 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
 - "27017:27017"
    extra_hosts:
 - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
 - MONGO_INITDB_ROOT_USERNAME=root
 - MONGO_INITDB_ROOT_PASSWORD=root
 - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
 - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
 - primary_data_db:/data
 - primary_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27017 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'
      interval: 5s

  replica_1:
    image: mongo:latest
    command: mongod --port 27018 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
 - "27018:27018"
    extra_hosts:
 - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
 - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
 - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
 - replica_1_data_db:/data
 - replica_1_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27018 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'

  replica_2:
    image: mongo:latest
    command: mongod --port 27019 --replSet rs0 --bind_ip_all --keyFile /data/configdb/keyfile
    ports:
 - "27019:27019"
    extra_hosts:
 - "host.docker.internal:host-gateway"
    restart: unless-stopped
    environment:
 - MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
    volumes:
 - ./keyfile.sh:/docker-entrypoint-initdb.d/keyfile.sh
 - replica_2_data_db:/data
 - replica_2_data_configdb:/data/configdb
    healthcheck:
      test: 'mongosh --quiet --port 27019 --eval "db.runCommand({ ping: 1 }).ok" | grep 1'

  init:
    image: mongo:latest
    restart: no
    extra_hosts:
 - "host.docker.internal:host-gateway"
    depends_on:
      primary:
        condition: service_healthy
      replica_1:
        condition: service_healthy
      replica_2:
        condition: service_healthy
    command: >
 mongosh -u root -p root --host host.docker.internal --port 27017 --eval '
 rs.initiate({
 _id: "rs0",
 members: [
 { _id: 0, host: "host.docker.internal:27017" },
 { _id: 1, host: "host.docker.internal:27018" },
 { _id: 2, host: "host.docker.internal:27019" }
 ]
 })
 '

volumes:
  primary_data_db:
  primary_data_configdb:
  replica_1_data_db:
  replica_1_data_configdb:
  replica_2_data_db:
  replica_2_data_configdb:

networks:
  default:
    name: mongodb_replica_set_network
Enter fullscreen mode Exit fullscreen mode

Now, we execute the following command inside the directory where our docker-compose.yml file is:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

That will pull the mongo:latest image from a container registry (if you don't already have it pulled locally) and create our volumes and networks.

You might have to wait a little before everything is up and running, but it usually takes no more than 7 seconds for the primary node and ~31 seconds for the secondaries to be ready.

Once everything is ready, open MongoDB Compass and connect to the replica set!

To do that, once MongoDB Compass is open, click on "Add new connection".

Image description

Expand the "Advanced Connection Options" dropdown and click the "Authentication" tab.

Image description

Input root username and password.

Image description

Click on "Advanced" and input the replica set name.

Image description

Click on "General" and add hosts for each replica set node.

Image description

Finally, click on "Save & Connect," and voila! You've successfully connected to your very own locally hosted MongoDB replica set!

You can now start adding and querying data in MongoDB Compass or using one of the many drivers available for many languages.

I hope you found this helpful.

Thanks for reading!

Top comments (0)