Building Docker images on GitLab CI: Docker-in-Docker and Podman
If you’re using GitLab CI to build your software, you might also want to use it to build Docker images of your application. This can be a little tricky, because by default GitLab CI runs jobs inside Docker containers.
The standard technique for getting around this problem is using Docker-in-Docker, but you can also use a simpler technique by using Podman, the reimplemented version of Docker. Let’s see why and how.
Option #1: Docker-in-Docker
When you run the
docker command-line tool, it is actually not doing much work itself.
Instead, it talks to
dockerd, a daemon or server typically running on the same machine where you’re running the CLI.
The actual work of running a container or building an image is done by
When you want to run
docker inside GitLab CI, you face the issue that GitLab CI jobs typically run as Docker containers.
So you can’t just rely on a normal
dockerd being available as you would, for example, in a virtual machine.
To help with this scenario, there’s a Docker image that runs
dockerd for you:
Once that is running, you can point
docker at that running daemon and issue commands like
In the context of GitLab CI, your jobs can run services, which are also Docker containers.
So we can configure
.gitlab-ci.yml to run
docker:dind as a service in a job:
dind-build: # ... services: - name: docker:dind alias: dockerdaemon
In this case, the service is given the hostname alias
You also need to tell the
docker CLI how to find the server, which you can do via an environment variable
DOCKER_HOST, as well as set a couple of other variables that make it work, and work faster:
dind-build: # ... variables: # Tell docker CLI how to talk to Docker daemon. DOCKER_HOST: tcp://dockerdaemon:2375/ # Use the overlayfs driver for improved performance. DOCKER_DRIVER: overlay2 # Disable TLS since we're running inside local network. DOCKER_TLS_CERTDIR: ""
A full configuration that builds an image and pushes it to the GitLab image registry corresponding to the GitLab CI repository looks like this:
stages: - build # Build and push the Docker image to the GitLab image # registry using Docker-in-Docker. dind-build: stage: build image: # An alpine-based image with the `docker` CLI installed. name: docker:stable # This will run a Docker daemon in a container # (Docker-In-Docker), which will be available at # thedockerhost:2375. If you make e.g. port 5000 public in # Docker (`docker run -p 5000:5000 yourimage`) it will be # exposed at thedockerhost:5000. services: - name: docker:dind alias: dockerdaemon variables: # Tell docker CLI how to talk to Docker daemon. DOCKER_HOST: tcp://dockerdaemon:2375/ # Use the overlayfs driver for improved performance. DOCKER_DRIVER: overlay2 # Disable TLS since we're running inside local network. DOCKER_TLS_CERTDIR: "" script: # GitLab has a built-in Docker image registry, whose # parameters are set automatically. You can use some # other Docker registry though by changing the login and # image name. - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - docker build -t "$CI_REGISTRY_IMAGE:dind" . - docker push "$CI_REGISTRY_IMAGE:dind"
For more details see the relevant GitLab CI documentation.
A working example
I’ve set up an example repository that contains this configuration.
Here’s what the
Dockerfile looks like:
FROM python:3.9-slim-bullseye RUN pip install cowsay ENTRYPOINT python -c "import cowsay; cowsay.tux('hello')"
Like most GitLab repositories, it has a corresponding Docker image registry, and you can run the image built by the above configuration like so:
$ docker run registry.gitlab.com/pythonspeed/building-docker-images:dind _____ | hello | ===== \ \ \ .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Note: Outside the very specific topic under discussion, the Dockerfiles in this article are not examples of best practices, since the added complexity would obscure the main point of the article.
To ensure you’re following all the best practices you need to have a secure, correct, fast Dockerfiles, check out the Python on Docker Production Handbook.
Option #2: Podman
Podman is a reimplemented version of Docker from RedHat. It supports the same command-line options, but has a fundamentally different architecture: unlike Docker, there is no daemon by default. The CLI does all the work itself.
That means we can do a much simpler GitLab CI config, without the service running the daemon:
stages: - build # Build and push the Docker image to the GitLab image registry # using Podman. podman-build: stage: build image: name: quay.io/podman/stable script: # GitLab has a built-in Docker image registry, whose # parameters are set automatically. You can use some # other Docker registry though by changing the login and # image name. - podman login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - podman build -t "$CI_REGISTRY_IMAGE:podman" . - podman push "$CI_REGISTRY_IMAGE:podman"
Notice all we had to do was change the
docker command-line to do
podman instead; they basically accept the same options.
A working example
The same example repository is also configured to use Podman. Again, you can run the resulting image:
$ docker run registry.gitlab.com/pythonspeed/building-docker-images:podman _____ | hello | ===== \ \ \ .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Docker-in-Docker (DinD) vs Podman
Which of these two should you choose? DinD gives you access to BuildKit, which has some useful features and performance improvements; Podman does not support all of them yet, though it does support build secrets.
On the other hand, running the DinD daemon adds some overhead, since another image has to be downloaded; the DinD build adds another 20 seconds of fixed overhead in my test. For less trivial builds this overhead probably will be overwhelmed by other factors.
If you don’t care about BuildKit’s additional features, using Podman is just a little bit simpler while offering the same user experience.
Finally, you could look into Buildah, which is how
podman build is implemented: it’s a tool specifically focused only on building images.
Learn how to build fast, production-ready Docker images—read the rest of the Docker packaging guide for Python.
Production Docker packaging is too complicated to learn from Google searches
With as much as a dozen different intersecting technologies, and an unknown number of details to get right, Docker packaging isn't simple, especially for production.
But you still need fast builds that save you time, and security best practices that keep you safe.
Take the fast path to learning best practices, by using the Python on Docker Production Handbook.
Free ebook: Introduction to Dockerizing for Production
Learn a step-by-step iterative DevOps packaging process in this free mini-ebook. You'll learn what to prioritize, the decisions you need to make, and the ongoing organizational processes you need to start.
Plus, you'll join my newsletter and get weekly articles covering practical tools and techniques, from Docker packaging to Python best practices.