Skip to content

A Simple Way to Run a MongoDB Replica Set in GitHub Actions

Generally, setting up a MongoDB instance in GitHub Actions is relatively straightforward.

You just need to define a service container, expose the correct port, and you will be able to connect to that container from any step in your workflow. Here’s an example of such a setup:

name: "Build"
on:
  push

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    services:
      mongo:
        image: mongo:6
        options: >-
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 27017:27017
    steps:
      - name: echo
        run: "echo hello"

That setup works well for a standalone server, but it becomes slightly more complex if you want to run a replica set. The complexity stems from the lack of support for specifying a command option in GitHub Actions.

MongoDB makes it pretty easy to convert a standalone instance into a single node replica set. Normally, running a single node replica set in Docker is as simple as:

$ docker run -d -p 27017:27017 --name mongo  mongo:6.0 --replSet test --bind_ip_all

Using Docker Compose:

version: '3'
services:
  mongodb-main:
    image: mongo:6
    command: --replSet test # Specify replica set
    healthcheck:
      test: mongosh --eval 'db.runCommand("ping").ok'
      interval: 2s
      retries: 5
      timeout: 2s
    ports:
      - 27017:27017

The --replset test option is what tells the container to join a replica set named test. In GitHub Actions, one would expect to be able to specify the replica set by defining a workflow that looks something like:

name: "Build"
on:
  push

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    services:
      mongo:
        image: mongo:6
        options: >-
          --health-cmd "mongosh --eval 'db.runCommand(\"ping\")'"
          --command "--replSet test" # Try to specify replica set
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 27017:27017
    steps:
      - name: echo
        run: "echo hello"

Unfortunately, this setup is not supported:

Error: Cannot parse container options: '--command --replSet test --health-interval 10s --health-timeout 5s --health-retries 5': 'unknown flag: --command'

Thankfully though, GitHub-hosted runners are just virtual machines that have Docker installed, so we can manually create the container in a workflow step and specify the replica set option there:

name: "Build"
on:
  push

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Start mongo
        id: start-mongo
        run: docker run --rm -d -p 27017:27017 --name mongo  mongo:6.0 --replSet test --bind_ip_all

We are now running a MongoDB replica set that is accessible to our tests or any scripts on the host runner at localhost:27017.

We are almost there, but there is one more task that we need to action. We now need to initiate the replica set by calling rs.initate() on the node. We can do this by relying on Docker once again and running mongosh, which is bundled with the official MongoDB image, in a separate container:

name: "Build"
on:
  push

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Start mongo
        run: docker run --rm -d -p 27017:27017 --name mongo  mongo:6.0 --replSet test --bind_ip_all
        
      - name: Initialize MongoDB Replica Set
        run: |
          sleep 5 # Give mongo a chance to start up
          docker run --rm mongo:6.0 mongosh --host 172.17.0.1 --eval 'rs.initiate({_id: "test", members: [{_id: 0, host: "172.17.0.1:27017"}]})'

And just like that, we have an initialized replica set up and running, ready to serve queries.

Networking considerations

Now, everything works as we expect, our replica set is up and running and we can connect to it. Note that there are a couple of subtleties that facilitate this, without which, the whole setup falls apart.

Firstly, what's with the --bind_ip_all option in the command to used to start the replica set? That is:

docker run -d -p 27017:27017 --name mongo  mongo:6.0 --replSet test --bind_ip_all # <-- what's this?

This option tells MongoDB to listen to all IPv4 interfaces. By default, MongoDB only listens to localhost, so you will only be able to connect to it from the container running the replica set and the host runner (-p 27017:27017 binds port 27017 of the container to 27017 of the host runner). However, we need to connect to the replica set from another container to initialize the replica set.

Looking closely at this command, we see that the host we want to connect to is 172.17.0.1, which is not a loopback address but rather the default Docker bridge gateway, which is our second subtlety.

docker run --rm mongo:6.0 mongosh --host 172.17.0.1 --eval 'rs.initiate({_id: "test", members: [{_id: 0, host: "172.17.0.1:27017"}]})'

Using this address, Docker will route our request to the correct container exposing our port. Had we used localhost, the running container would have tried to connect to itself, and since it is not running a MongoDB instance, the attempt would have failed.

Full example

In the interest of completeness, here is a full example workflow:

name: "Build"
on:
  push

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Start mongo
        id: start-mongo
        run: docker run --rm -d -p 27017:27017 --name mongo  mongo:6.0 --replSet test --bind_ip_all

      - name: Initialize MongoDB Replica Set
        run: |
          sleep 5 # Give mongo a chance to start up
          docker run --rm mongo:6.0 mongosh --host 172.17.0.1 --eval 'rs.initiate({_id: "test", members: [{_id: 0, host: "172.17.0.1:27017"}]})'
      - name: Test MongoDB
        run: |
          docker run --rm mongo:6.0 mongosh --host 172.17.0.1 --eval 'db.testDocs.insertOne({ok: true})'
      - name: Stop MongoDB
        run: |
          docker rm -f mongo