Quick and Easy MongoDB Replica Set Deployment with Docker Compose
Abdelfattah Radwan
Posted on July 15, 2024
For the past few weeks, in my free time, I wanted to learn how to set up a MongoDB replica set that I could use for development or production, but I was frustrated by the resources online.
Most were outdated or didn’t explain how to set up authentication using key files, which is required when deploying replica sets.
So, without further ado, let’s get started!
First of all, you will need to download and install Docker. I downloaded & installed Docker Desktop for Windows.
Next, you need to open up a Windows Terminal (or CMD.exe) window, and run the following command: docker image pull mongo:latest
. This will pull the latest version of the mongo:latest
image.
You can also use Docker Desktop to do that from the GUI. Open up the Docker Desktop application, search for mongo:latest, select "latest" from the "Tag" dropdown, and click "Pull".
Once the image is pulled successfully, create a new directory named "mongodb" anywhere on your machine.
Inside that directory, create a new file named docker-compose.yml and open it up in your preferred text/source code editor. I will be using VSCode.
Let's begin by specifying the name of our Docker Compose project.
name: mongodb-replica-set
Then, let’s define our first MongoDB replica set instance:
services:
mongodb-1:
image: mongo:latest
container_name: mongodb-1
command:
[
"mongod",
"--port",
"27017",
"--replSet",
"rs0",
"--bind_ip_all",
"--keyFile",
"/data/configdb/keyfile",
]
ports:
- "27017:27017"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=root
- MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
volumes:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-1:/data/db"
- "./data/mongodb-1/configdb:/data/configdb"
You might be a little intimidated by the size of this “simple” config but fear not, let’s go through it:
- Image and Container Name: We use the latest MongoDB image and name this container mongodb-1.
- Command: This starts the MongoDB server (mongod) with specific options:
- port 27017: Sets the port to 27017.
- replSet rs0: Specifies the replica set name as rs0.
- bind_ip_all: Binds MongoDB to all available IP addresses.
- keyFile /data/configdb/keyfile: Specifies the key file for authentication.
- Ports: Maps port 27017 of the container to port 27017 on the host machine.
- Extra Hosts: Adds an entry to the container’s /etc/hosts file.
- Restart Policy: Ensures the container always restarts.
- Environment Variables: Sets the root username, password, and the replica set key.
- Volumes: Mounts necessary files and directories from the host to the container.
On Windows, although not required, I suggest you add the "host.docker.internal:host-gateway"
to the extra_hosts
attribute. This allows drivers and apps like MongoDB Compass to resolve addresses of other replica set instances. On Linux, that is not required as far as I know.
You might have also noticed that we have a custom environment variable called MONGO_REPLICA_SET_KEY
. It holds the key that will be put inside our instances’ key files. The key file itself is created using the init-keyfile.sh
script. The script simply copies the value of MONGO_REPLICA_SET_KEY
into a file named "keyfile" located at /data/configdb
and sets the correct permissions and owner of the file.
#!/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
You can generate a key using the following command (taken from the MongoDB docs):
openssl rand -a -base64 756
Copy and paste the generated key into a .env
file:
MONGO_REPLICA_SET_KEY="YOUR_REPLICA_SET_KEY"
Now, let’s define a couple more MongoDB instances for our replica set:
mongodb-2:
image: mongo:latest
container_name: mongodb-2
command:
[
"mongod",
"--port",
"27018",
"--replSet",
"rs0",
"--bind_ip_all",
"--keyFile",
"/data/configdb/keyfile",
]
ports:
- "27018:27018"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: always
environment:
- MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
volumes:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-2:/data/db"
- "./data/mongodb-2/configdb:/data/configdb"
mongodb-3:
image: mongo:latest
container_name: mongodb-3
command:
[
"mongod",
"--port",
"27019",
"--replSet",
"rs0",
"--bind_ip_all",
"--keyFile",
"/data/configdb/keyfile",
]
ports:
- "27019:27019"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: always
environment:
- MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
volumes:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-3:/data/db"
- "./data/mongodb-3/configdb:/data/configdb"
Both of these two instances are pretty much the same as the first one with only minor differences.
Now, let’s define a fourth MongoDB instance that will wait for the first instance to start before initiating the replica set.
mongodb-init:
image: mongo:latest
container_name: mongodb-init
depends_on:
- mongodb-1
- mongodb-2
- mongodb-3
extra_hosts:
- "host.docker.internal:host-gateway"
command: bash -c "chmod +x init-replica-set.sh && ./init-replica-set.sh"
volumes:
- "./init-replica-set.sh:/init-replica-set.sh"
restart: no
Pretty simple, eh? This instance is configured to wait for the other three instances’ containers to start before it starts and executes a script called init-replica-set.sh
. Here is said script:
#!/bin/bash
echo "Waiting for MongoDB to start on host.docker.internal:27017..."
until mongosh --quiet --host host.docker.internal --port 27017 --eval "db.runCommand({ ping: 1 }).ok" | grep 1 &>/dev/null; do
sleep 1
done
echo "MongoDB has started successfully"
echo "Initiating MongoDB replica set..."
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'
}
]
})
"
Again, don’t worry if it looks intimidating, let’s go through it:
echo "Waiting for MongoDB to start on host.docker.internal:27017..."
until mongosh --quiet --host host.docker.internal --port 27017 --eval "db.runCommand({ ping: 1 }).ok" | grep 1 &>/dev/null; do
sleep 1
done
echo "MongoDB has started successfully"
- This block waits until the MongoDB instance on port 27017 is up and running. It checks by pinging the MongoDB server and waiting for a successful response.
- The loop continues to check every second (sleep 1) until a successful ping response is received.
echo "Initiating MongoDB replica set..."
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'
}
]
})
"
- This block initiates the replica set using the mongosh command.
- It connects to the MongoDB instance on port 27017 using the root credentials.
- The rs.initiate command sets up the replica set with the specified members (host.docker.internal:27017, host.docker.internal:27018, and host.docker.internal:27019).
Note that we used host.docker.internal
and NOT the container name as you’d usually do when working with containers. This again is only required on Windows.
The complete docker-compose.yml
file should look something like this:
name: mongodb-replica-set
services:
mongodb-1:
image: mongo:latest
container_name: mongodb-1
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:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-1:/data/db"
- "./data/mongodb-1/configdb:/data/configdb"
mongodb-2:
image: mongo:latest
container_name: mongodb-2
command:
[
"mongod",
"--port",
"27018",
"--replSet",
"rs0",
"--bind_ip_all",
"--keyFile",
"/data/configdb/keyfile",
]
ports:
- "27018:27018"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: always
environment:
- MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
volumes:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-2:/data/db"
- "./data/mongodb-2/configdb:/data/configdb"
mongodb-3:
image: mongo:latest
container_name: mongodb-3
command:
[
"mongod",
"--port",
"27019",
"--replSet",
"rs0",
"--bind_ip_all",
"--keyFile",
"/data/configdb/keyfile",
]
ports:
- "27019:27019"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: always
environment:
- MONGO_REPLICA_SET_KEY=${MONGO_REPLICA_SET_KEY}
volumes:
- "./init-keyfile.sh:/docker-entrypoint-initdb.d/init-keyfile.sh"
- "./data/mongodb-3:/data/db"
- "./data/mongodb-3/configdb:/data/configdb"
mongodb-init:
image: mongo:latest
container_name: mongodb-init
depends_on:
- mongodb-1
- mongodb-2
- mongodb-3
extra_hosts:
- "host.docker.internal:host-gateway"
command: bash -c "chmod +x init-replica-set.sh && ./init-replica-set.sh"
volumes:
- "./init-replica-set.sh:/init-replica-set.sh"
restart: no
networks:
default:
name: mongodb-replica-set-network
Finally, run the following command in the directory containing the docker-compose.yml
file: docker compose up -d
You might have to wait a bit before everything starts up, but afterwards, you can connect to your replica set via one of the drivers or MongoDB Compass. I will use MongoDB Compass to demonstrate the final result.
Open MongoDB Compass and enter the following connection string: mongodb://root:root@localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0
Click on “Connect” and voila! MongoDB Compass will present you with a screen like this:
That’s it! Now, you have a fully functional MongoDB replica set! However, running all instances of the replica set on the same machine defeats the purpose of replica sets: redundancy and data availability. So, I advise you to spread your instances across different machines or create a single-instance replica set for local development.
Posted on July 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.