GitLab CI’s docker:dind service is a powerful tool for building Docker images within your CI/CD pipelines, but it’s also a common source of confusion and frustration.

Let’s see it in action. Imagine you have a Dockerfile like this:

FROM alpine:latest
RUN apk add --no-cache curl
CMD ["echo", "Hello from Docker!"]

And your .gitlab-ci.yml looks like this:

stages:
  - build

docker_build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

When this pipeline runs, GitLab spins up a docker:dind (Docker-in-Docker) service container. This dind container is essentially a fully functional Docker daemon, isolated from the host. Your main CI job container (docker:latest in this case) then communicates with this dind daemon using the Docker CLI. The docker build command executes inside the dind container, pulling base images, creating layers, and finally tagging your new image. The docker login and docker push commands interact with the dind daemon, which then pushes the image to your GitLab Container Registry.

The core problem docker:dind solves is enabling Docker daemon operations within an environment that itself might be containerized or managed by an orchestrator like Kubernetes. Without it, your CI runner would need direct access to a Docker host, which is often not feasible or desirable for security and isolation reasons. dind provides a self-contained Docker environment for each job.

The primary levers you control are:

  • image: docker:latest: This specifies the image for your job container. It needs to contain the Docker CLI. docker:latest is the standard choice.
  • services: - docker:dind: This is the crucial part. It tells GitLab to provision a separate container running a Docker daemon (docker:dind) and link it to your job container.
  • Docker CLI commands: All docker commands within your script section are directed to the dind service.

The most surprising thing about docker:dind is that it’s not a true "Docker inside Docker" in the strictest sense. It doesn’t run a full nested kernel. Instead, it uses Linux kernel features like namespaces and cgroups to simulate isolation and run Docker commands within the job container, but managed by a separate Docker daemon. This means it’s generally safe and doesn’t require privileged access on the host machine, unlike some older "Docker outside Docker" methods.

The key to successfully using docker:dind lies in ensuring the Docker CLI in your job container can communicate with the dind service. This communication typically happens over a Unix socket (/var/run/docker.sock).

The next thing you’ll likely encounter is optimizing build times by leveraging Docker layer caching.

Want structured learning?

Take the full Gitlab-ci course →