Systems & tooling
CircleCI / Docker in docker executor
Krisztián Gulyás 3 min read
A step-by-step guide to setting up Docker-in-Docker with CircleCI for running containerized tests with external dependencies.

Table of Contents
Preface
If are you using CirceCI as a continuous integration tool, and your application has external dependencies either as a database
or a mocked service already containerized, there are two ways you can choose. The easy way is to have a machine
executor, without any pain and gain. The ubuntu image already comes with installed docker, you can use it, all you
need is to install docker-compose if you need it. The hard way is to set up a docker executor, to build, compose and
run your components (spoiler: and your tests too).
Limitations
- CircleCI doesn’t support
volumes. It means, for instance, you cannot define your SQL-init script with it. - Another pain point, is the networking. Your primary container can’t reach other containers through the docker network.
- Waiting for container startup can be solved by third-party extensions, but no direct support for it from CircleCI.
Step by step
Choosing the right image
The primary image for CircleCI is where commands are executed from config.yml. I’d prefer something which already
has docker cli, for instance:
test-circle-ci:
docker:
- image: docker:17.05.0-ce-git
resource_class: small
The docker docker image is an Alpine-based image, with a minimal footprint. You don’t have neither curl nor docker-compose
Checkout
Ok, it does nothing with the dockerized configuration, but it should be done.
steps:
- checkout
...
Setup remote docker
This is where it begins. You will not use the docker daemon from your primary container. This command connects your
container with CircleCI’s remote docker solution.
steps:
...
- setup_remote_docker:
docker_layer_caching: true
version: 20.10.12
...
Without the version parameter you can experience connection issues from docker-compose. Which is tricky,
because docker build is still working without it.
Install docker-compose
As I already mentioned it in the image part, the chosen image is so small, it doesn’t have a curl,
a docker-compose or sudo. At least you already root in the image.
steps:
...
- run:
name: Download and install docker compose
command: |
apk update && apk upgrade && apk add curl
curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64" -o ~/docker-compose
chmod +x ~/docker-compose
mv ~/docker-compose /usr/local/bin/docker-compose
...
Building the test container
Because you cannot connect to the remotely executed docker containers from your CircleCI primary image, you need a test
container, which can be executed as part of the docker-network. I hope you already prepared a Dockerfile in your
project root. Keep it simple as possible, copy your sources, resources, dependencies, test and config files, and set up
your WORKDIR, and environment variables.
steps:
...
- run:
name: Build test image
command: |
docker build -t tests .
...
Preparing the dependencies
Do you have a docker-compose.yaml already? It’s time to spin it up!
steps:
...
- run:
name: Spin up database
command: |
docker-compose down -v
docker-compose up -d
...
Looks easy, doesn’t it? But don’t forget about the missing feature, the volumes are not supported via remote-docker
Init database
Let’s wait while the database spins up, and initialize it with docker exec command
steps:
...
- run:
name: prepare database
command: |
while ! docker exec database mysql -e 'select 1=1' -proot test_db; do
echo "waiting for database"
sleep 2
done
docker cp test/resources/init.sql database:/init.sql
docker exec database mysql -e 'source /init.sql' -proot test_db
...
Execute all the tests!
I’m a Clojure developer, so I have an alias in my deps.edn for circe-ci-test. Let’s run it.
steps:
...
- run:
name: testing
command: |
docker run --network container:database tests clj -M:circle-ci-test
...
Tear down
Be a nice guy, and clean up the mess you did:
steps:
...
- run:
name: tear-down
command: |
docker-compose down -v
...
All together
version: 2.1
jobs:
test_ci:
docker:
- image: docker:17.05.0-ce-git
resource_class: small
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
version: 20.10.12
- run:
name: Download and install docker compose
command: |
apk update && apk upgrade && apk add curl
curl -L "https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64" -o ~/docker-compose
chmod +x ~/docker-compose
mv ~/docker-compose /usr/local/bin/docker-compose
- run:
name: Build test image
command: |
docker build -t tests .
- run:
name: Spin up database
command: |
docker-compose down -v
docker-compose up -d
- run:
name: prepare database
command: |
while ! docker exec database mysql -e 'select 1=1' -proot test_db; do
echo "waiting for database"
sleep 2
done
docker cp test/resources/init.sql database:/init.sql
docker exec database mysql -e 'source /init.sql' -proot test_db
- run:
name: testing
command: |
docker run --network container:database tests clj -M:circle-ci-test
- run:
name: tear-down
command: |
docker-compose down -v
workflows:
version: 2
ci_tests:
jobs:
- test-circle-ci:
filters:
branches:
ignore: master
test-deploy:
jobs:
- test_ci:
filters:
branches:
only: master
- deploy:
requires:
- test_ci
filters:
branches:
only: master
Epilogue
Of course these steps come only from my experience, you can have different problems and solutions for it. These
steps are working for me even on CirceCI cloud, and at my local with command.
circleci local execute --job test-circle-ci
References
- Race conditions: wait for the database
- CircleCI’s “Building Docker images” guide and its “How can I mount volumes to Docker containers?” support article were the other two references. Both have since been retired by CircleCI and are not linked here, rather than pointed at a 404.
Tagged