Running tests in containers with docker-compose

The main advantages of this way are to have independent environment for the tests running and to reduce the complexity of the test environment setup. Just load and run tests. Consider how to achieve this.

You will see below how you can setup the docker-compose for common Ruby on Rails application. As a bonus, you will be able to reuse those setup on all projects without much changing.

What we want to achieve

Running the tests should be easy. New developers should be able to join the development process without much trouble setting up the test environment. And it can become relatively complex including a set of integration with a bunch of additional services. For example, it requires a running Database.

Test runs should be isolated and repeatable. You don’t want to have that one flaky test that fails only on your machine. A failing test should fail when it’s run by anyone in the team and on CI as well.

Test environment should be as close to the production environment as possible. Having green system integration tests should guarantee that a feature works in production. As your application grows the number of service dependencies might grow as well, and it’s important to verify that integrations with them work correctly and keep the dependencies up to date.

Enter Docker

Utilizing Docker to run tests can help you to solve these problems. All developers will have the same isolated test environment setup, which can be used for running tests on CI as well. New developers won’t need to spend half a day just to set up everything required to run tests.

Tests will be run inside a container, so you’ll need to define one. It’s done in a Dockerfile:

FROM cimg/ruby:2.7.1


RUN sudo apt-get update -qq \
  && sudo apt-get install -yq --no-install-recommends \
      libxml2-dev libxslt-dev libtool pkg-config \
      libbz2-dev libglib2.0-dev libxml2-dev libxslt-dev cmake \
  && sudo apt-get clean \
  && sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
  && sudo truncate -s 0 /var/log/*log


RUN gem update --system && gem install rake bundler --no-document

ADD${TINI_VERSION}/tini /usr/local/bin/tini
RUN sudo chmod a+x /usr/local/bin/tini

ENTRYPOINT ["/usr/local/bin/tini", "--"]

First, define the base image, we use the official CircleCI image here. Then install the required application dependencies and configure ownership and rights for files from mounted volumes (more on that later). In the end, define the tini entrypoint.

tini is a neat tool that helps to reap any zombie processes and forward signals to commands executed in the container.

Wiring up

Running application tasks might require additional services dependencies. In this example, in order to run tests, the app needs an accessible database instance. It can be provided by utilizing the docker-compose.

version: "3.8"

      context: .
      dockerfile: ./Dockerfile
      DATABASE_URL: postgresql://root@db:5432/db_name
      PGHOST: db
      PGUSER: root
      RAILS_ENV: ${RAILS_ENV:-test}
    user: ${CURRENT_UID:?"Please run as follows 'CURRENT_UID=$(id -u):$(id -g) docker-compose up'"}

      - .:/home/circleci/project:cached
      - gems:/home/circleci/.rubygems
      - cache:/home/circleci/.cache

      - db

    image: circleci/postgres:alpine
    restart: always
      POSTGRES_USER: root
      POSTGRES_DB: db_name
      - pg:/var/lib/postgresql/data

Here we define two services: app and db. App service is built using the defined Dockerfile, all required environment variables are set here as well.

In the volumes section firstly the application directory is mounted, the rest volumes are added for caching purposes. Here also specified that app service depends on db service.

Please note that we specify the user for the app service and that user should be passed by the caller of the docker-compose command.

It’s done in order to solve the permissions problem, which can occur when processes need to modify files inside the mounted volumes, or when you’ll need to access the files (logs, screenshots, other artifacts) which were generated inside the container. To solve both these problems you need to pass your current user uid to the docker-compose commands.

The db service configuration consists of image name (provided by CircleCI), DB access credentials in environment variables, and volume to store the DB data.


Docker and docker-compose do a lot of work to make life easier for developers now, but it’s still a lot to remember and type:

    export CURRENT_UID=$(id -u):$(id -g)

    docker-compose up --remove-orphans -d db

    docker-compose build app

    docker-compose run app bin/bundle install

    docker-compose run app bin/rails db:prepare

    docker-compose run app bin/rails bin/rails db:schema:load

    docker-compose run app bin/rspec

All these can be extracted into a few utility scripts:

#!/usr/bin/env bash

echo "=>  Install dependencies"
bin/bundle install

echo "=>  Prepare DB"
bin/rails db:prepare
bin/rails db:schema:load
#!/usr/bin/env bash

echo "=>  Build"

export CURRENT_UID=$(id -u):$(id -g)

docker-compose up --remove-orphans -d db
docker-compose build app

echo "=>  Setup"
docker-compose run app bin/ci-setup
#!/usr/bin/env bash

echo "=>  Run tests"

export CURRENT_UID=$(id -u):$(id -g)

docker-compose run app bin/rspec

Now setting up and running tests inside a docker container is achieved by only running:



Docker is a powerful tool to use in your development process. It can make starting and switching between projects fast and easy, and help to ensure that everyone is staying up to date with the technologies being used.

Dmitry Tsvetkov is a Software Engineer at JetThoughts. Follow him on LinkedIn or GitHub.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories.