avatar

Krisztián Gulyás

Posted on 18th May 2022

CircleCI / Docker in docker executor

news-paper Clojure | Software Development |

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

https://circleci.com/docs/2.0/building-docker-images/
https://support.circleci.com/hc/en-us/articles/360007324514-How-can-I-mount-volumes-to-docker-containers
https://support.circleci.com/hc/en-us/articles/360006773953-Race-Conditions-Wait-For-Database