How to Run Tests for Your Containerized Monorepo Using Github Actions

Recently I’ve discovered the convenience of using Github actions for DevOps tasks, namely automated testing. With a simple configuration file, our project’s Github monorepo will automatically run a test suite for our web application. Our project team is now more confident in the changes we deploy and, most importantly, all while having little baggage added to the project.

In this post, I’ll show you the automated Github workflow testing strategy in use on our current project. Your project structure likely looks different than this one. However, the Github workflow implementation should be similar if each service in your monorepo is containerized.

Project architecture

This monorepo consists of a Rails backend, React frontend, MariaDB relational database, and an Nginx reverse proxy to tie the back and front end together on a single network port, 8080. Each service with its own Dockerfile and a docker-compose file to spin up everything at once. Here’s what the root directory of the project looks like:

├── .github/workflows/run_tests.yml # github workflow test config
├── backend # rails backend API
├── bin # project helper executables
├── frontend # react frontend
├── infrastructure # terraform infra
├── test # cypress e2e tests
├── docker-compose.yml # docker-compose for local dev
└── tests-docker-compose.yml # docker-compose for testing dist builds

There’s one thing to note. Our Cypress E2E tests (i.e. the test directory) are not containerized because it provides a user interface and requires a web browser to run the tests against. Both of these, I find, make it difficult to run inside a container. Luckily, Cypress has a premade Github action that makes it trivial to use in a workflow.

Having the project structure in mind, let’s move on to our test suite setup.

Test suite setup

We have unit and integration tests for the backend using the Rails built-in scaffolding. And for E2E testing, we have an isolated Cypress test suite in the test directory to perform interactions on the frontend to assert that the data travels through the entire system as expected.

The E2E test interacts with the web application via a headless browser similar to how an end-user would interact with the app. The goal is to ensure the core functionality of the app does not break when releasing new code changes.

Once you have developed the tests for your application to ensure your app functions correctly, it’s time to start considering how you can streamline the testing process into your Github workflows.


We want to achieve two essential goals by automating the test suite:

  1. Automatically run the test suite whenever someone pushes to the main branch or opens up a pull request to the main branch.
  2. Update the status checkmark on the latest Git commit to indicate if the test suite passed or not.

Here are the actions the Github workflow performs for the goals mentioned above.

  1. Checkout the source code.
  2. Set up the required ENV variables for running all the Docker containers.
  3. Start all the containers using the docker-compose file and initialize the database with data.
  4. Run the unit and integration tests.
  5. Run the system E2E tests.
  6. Update the commit status to indicate if the workflow passed or failed.

And here is what this implementation looks inside of our .github/workflows/run_tests.yml file:

name: Run tests
      - main
      - main

    runs-on: ubuntu-latest
      RAILS_ENV: test
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup env
        run: |
          echo "PROJECT_NAME=combine" >> $GITHUB_ENV
          echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH

      - name: Start docker containers
        run: |
          echo "VERSION=$(versioninfo)" >> $GITHUB_ENV
          docker-compose -f tests-docker-compose.yml up --build --detach
          sleep 10  # wait for database to be ready
          docker-compose -f tests-docker-compose.yml run backend bash -c "rails db:create && rails db:migrate && rails db:seed"

      - name: Run backend unit and integration tests
        run: |
          docker-compose -f tests-docker-compose.yml run backend rails test

      - name: Run cypress e2e tests
        uses: cypress-io/github-action@v4
          working-directory: test
          wait-on: "http://localhost:8080"

      - if: always()
        uses: ouzi-dev/[email protected]
          name: "Run tests"
          status: "${{ job.status }}"

If all goes well, this Github workflow should be triggered automatically by Git push events. At the end, there should be a green checkmark on the Git commit!

For now, this testing workflow works fine, but there’s certainly room for improvement. For instance:

  • Both our testing and deploy workflow build the production Docker images. This hasn’t given us trouble yet, but technically, the image that’s deployed isn’t the exact image that was tested against, even though the Docker images were built using the same source code.
  • It currently takes two and a half minutes to download and build the Docker images. I believe there are ways to cache certain parts of the Docker images to make this faster.
  • We need to figure out a way to use a single docker-compose file for spinning up both a development and testing environment. The tricky part here is in our development environment, the frontend uses a Vite server, and in production, the frontend is Nginx serving the bundled HTML, CSS, and Javascript.

Using Github Actions

Overall, I was pretty happy to learn that a lot of the work done to containerize our app for local development and production deployments could easily be leveraged for automated testing using Github actions. I found the setup to be simple while providing our team with confidence in our deployments. Hopefully, this example can help you implement a testing workflow on your project!

  • Guillaume says:

    you probably want to use `docker compose up –wait` rather than `sleep` :)

    • Nathan Papes Nate says:

      Hey Guillaume, appreciate the suggestion, and I agree that the “–wait” flag is a better solution than blocking for an arbitrary amount of seconds.

      I did try this out. It appears “–wait” is available in docker-compose version 2.1 and up, and the ubuntu-latest (22.04, currently) runner image I’m using has docker-compose version 1.29.2 installed by default. Unfortunately this flag isn’t readily available to my github action (yet!).

  • Piotr says:

    The Ubuntu 22.04 runner image has Docker Compose v2 (2.17.2) installed too. Use `docker compose` instead of `docker-compose`, e.g.:

    docker compose -f tests-docker-compose.yml up –build –detach –wait

    I hope that helps.

  • Comments are closed.