Create Mongo Replicaset using Docker Compose

In this blog post, we will attempt to create MongoDb replicaset using docker compose. A replica set in mongodb is a group of mongod instances which maintains the same set of data. This enables applications to work despite one of the db servers going down.

At every instance, there wil be only and only ONE primary node. Rest of the instances are deemed secondary. We would do write operations only via the primary node. The primary node records all the changes using the oplog, or in other words the operations log. The secondary replicates the changes in the oplog of primary node and replicate the data. Do note that this is an asynchronous operation.

When the primary node is unable to communicate, an election ensures another node is selected is primary. From that point onwards, the new node assumes the role of primary and would act as the entry point of data.

Implementing Mongo ReplicaSet

For this tutorial we will be using Docker for implementing mongodb. Infact, the focus would be on docker compose and how to use it to implement replicaset using mongodb containers. We will be implementing a replicaset of size 3 (nodes). we will begin by defining the network the containers would share and also the volume for each of the containers.

version: '3.8'

networks:
  common.network:
    driver: bridge

volumes:
  mongo.one.vol:
    name: "mongo.one.vol"

  mongo.two.vol:
    name: "mongo.two.vol"

  mongo.three.vol:
    name: "mongo.three.vol"

It is now time to define the 3 services required. Each of the services are mapped to their respective volume and is linked via a common network.

services: 
  mongo.one.db:
    container_name: mongo.one.db
    image: mongo:latest
    networks:
      common.network:
    ports:
      - 20000:27017
    volumes:
      - mongo.one.vol:/data/db
      # - ./rs-init.sh:/scripts/rs-init.sh
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]

  mongo.two.db:
    container_name: mongo.two.db
    image: mongo:latest
    networks:
      common.network:
    ports:
      - 20001:27017
    depends_on:
      - mongo.one.db
    volumes:
      - mongo.two.vol:/data/db
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]

  mongo.three.db:
    container_name: mongo.three.db
    image: mongo:latest
    networks:
      common.network:
    ports:
      - 20002:27017
    depends_on:
      - mongo.one.db      
    volumes:
      - mongo.three.vol:/data/db
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]

The most interesting part of the above docker compose is the --replSet command in entrypoint. This specifies the replica set name for the containers. Notice that we have specified same replica set name for all three containers (dbrs).

We are not done yet. We have specified the replicaset name, but we are yet to initiate the replica set. For this, we need to run the rs.initiate() command from mongosh.

we will use a batch file for the same. The batch files would initiate the docker compose, and then run the rs.initiate() command on one of the containers.

// startdb.bat

docker-compose up -d

timeout  /t 10 /nobreak

docker exec -it mongo.one.db mongosh --eval "rs.initiate({_id:'dbrs', members: [{_id:0, host: 'mongo.one.db'},{_id:1, host: 'mongo.two.db'},{_id:2, host: 'mongo.three.db'}]})"

Run the batch files and see how the replica set is built. You could ensure the replicaset is initated properly by running the rs.status() command on one of the containers.

 docker exec -it mongo.one.db mongosh --eval "rs.status()"

If your replicaset was successfully initiated, you would recieve a response as following.

{
  set: 'dbrs',
  date: ISODate("2023-02-13T14:53:31.692Z"),
  myState: 1,
  term: Long("3"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
    lastCommittedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
    appliedOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
    durableOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
    lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
    lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z")
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1676299994, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: ISODate("2023-02-13T14:52:34.888Z"),
    electionTerm: Long("3"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1676218262, i: 1 }), t: Long("2") },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    numCatchUpOps: Long("0"),
    newTermStartDate: ISODate("2023-02-13T14:52:34.899Z"),
    wMajorityWriteAvailabilityDate: ISODate("2023-02-13T14:52:35.451Z")
  },
  members: [
    {
      _id: 0,
      name: 'mongo.one.db:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 69,
      optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
      optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
      lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1676299954, i: 1 }),
      electionDate: ISODate("2023-02-13T14:52:34.000Z"),
      configVersion: 1,
      configTerm: 3,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'mongo.two.db:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 67,
      optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
      optimeDurable: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
      optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
      optimeDurableDate: ISODate("2023-02-13T14:53:24.000Z"),
      lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      lastHeartbeat: ISODate("2023-02-13T14:53:30.959Z"),
      lastHeartbeatRecv: ISODate("2023-02-13T14:53:29.991Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo.one.db:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 3
    },
    {
      _id: 2,
      name: 'mongo.three.db:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 66,
      optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
      optimeDurable: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
      optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
      optimeDurableDate: ISODate("2023-02-13T14:53:24.000Z"),
      lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
      lastHeartbeat: ISODate("2023-02-13T14:53:30.959Z"),
      lastHeartbeatRecv: ISODate("2023-02-13T14:53:29.991Z"),
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo.one.db:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 3
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1676300004, i: 1 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1676300004, i: 1 })
}

Notice the stateStr property of containers. One (and ONLY ONE) of the containers have the property set as PRIMARY, while rest of the members are marked as SECONDARY. In the above output, we can notice that the instance mongo.one.db has been currently elected as the primary node.

So how do one test this ? You could do the following.

  1. Connect the current primary instance and create a document (create db and collection if it doesn’t exists).
  2. Check if entry has been created in the other instances as well.
  3. Pause or stop the primary instance.
  4. Run the rs.status() command on one of the remaining nodes.
  5. Notice another node has been chosen as primary.
  6. Switch to current primary and create another document.
  7. Swithc on the previosly stopped container and ensure the new changes are synced (asynchronously)

That’s all for now. 🙂

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s