As those of you who have used MongoDB extensively have probably experienced, it is an incredibly...
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