Docker-in-Docker (DinD) in GitLab CI is one of those things that works perfectly once it's set up correctly — and is absolutely bewildering when it doesn't. The error messages are cryptic, the configuration has multiple interacting pieces, and most tutorials gloss over the parts that actually break.
This is a reference checklist. The 3 most common DinD failure modes, with the actual log output you'll see, and the exact fix for each.
What DinD Is and Why GitLab CI Needs It
When your CI job needs to build Docker images, it needs access to a Docker daemon. But your CI job is already running inside a container on the GitLab runner. So you need Docker inside Docker — hence "DinD."
GitLab supports this through the docker:dind service, which runs a Docker daemon as a sidecar container alongside your job. Your job container connects to this sidecar's Docker socket to run Docker commands.
This architecture has three points of failure, and each one produces a different error.
Failure 1: Missing or Misconfigured DinD Service
Log output you'll see:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
ERROR: error during connect: Post "http://docker:2376/v1.24/build": dial tcp: lookup docker on 10.0.0.10:53: no such host
Cause: The docker:dind service isn't configured, or the job is trying to connect to the wrong Docker host.
Fix:
build_image:
image: docker:24.0 # Use the Docker CLI image
services:
- docker:24.0-dind # Run DinD as a service
variables:
DOCKER_HOST: tcp://docker:2376 # Connect to the DinD service
DOCKER_TLS_CERTDIR: "/certs" # Enable TLS
DOCKER_CERT_PATH: "/certs/client" # Client cert location
DOCKER_TLS_VERIFY: "1" # Verify TLS
script:
- docker info
- docker build -t myapp .
Key points:
- The
imageandservicesversions should match (bothdocker:24.0) DOCKER_HOSTmust betcp://docker:2376(TLS) ortcp://docker:2375(no TLS) — notunix:///var/run/docker.sock- The hostname
dockeris a special alias that GitLab sets up for the service container
Failure 2: TLS Certificate Misconfiguration
Log output you'll see:
error during connect: Post "https://docker:2376/v1.24/images/create": tls: failed to verify certificate: x509: certificate signed by unknown authority
ERROR: error during connect: Get "https://docker:2376/v1.24/containers/json": write tcp 172.17.0.3:54120->172.17.0.2:2376: write: connection reset by peer
Cause: The DOCKER_TLS_CERTDIR variable is misconfigured. When DinD starts with TLS enabled, it generates certificates in /certs. These need to be shared with the client container via a volume. If the variable is empty, set wrong, or the volume mount doesn't work, TLS handshakes fail.
Fix — Option A: Configure TLS correctly (recommended):
variables:
DOCKER_TLS_CERTDIR: "/certs"
services:
- docker:24.0-dind
# The /certs directory is shared between the service and job containers
# via GitLab's automatic service volume sharing
Fix — Option B: Disable TLS entirely (simpler but less secure):
variables:
DOCKER_TLS_CERTDIR: "" # Empty = no TLS
DOCKER_HOST: tcp://docker:2375 # Port 2375 = no TLS
services:
- docker:24.0-dind
Option B is fine for CI — you're communicating between containers on the same host, so TLS isn't adding meaningful security. But option A is better practice.
Failure 3: Runner Privileged Mode Not Enabled
Log output you'll see:
ERROR: Cannot start service docker: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: Running in a user namespace with a privileged service container is not supported
Or sometimes just:
ERROR: Job failed: prepare environment: Error response from daemon: authorization denied by plugin
Cause: DinD requires the Docker container to run in privileged mode. If the GitLab runner isn't configured to allow privileged containers, the DinD service can't start.
Fix: This requires runner configuration changes (you can't fix this in .gitlab-ci.yml):
# /etc/gitlab-runner/config.toml
[[runners]]
[runners.docker]
privileged = true # Required for DinD
volumes = ["/certs/client"] # Share TLS certs
If you're using GitLab.com's shared runners, privileged mode is already enabled. If you're using self-hosted runners, you (or your platform team) need to enable it.
Security note: Privileged containers have full access to the host kernel. This is inherently risky. If this concerns you, consider the alternatives below.
When to Avoid DinD Entirely
DinD works, but it has real downsides: security concerns with privileged mode, performance overhead from the nested Docker daemon, and the configuration complexity you've just seen. Alternatives:
Kaniko (Google)
build_image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.22.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile Dockerfile
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
Kaniko builds images without a Docker daemon. No privileged mode needed. It's slower for complex builds but safer and simpler to configure.
Buildah
build_image:
stage: build
image: quay.io/buildah/stable
variables:
STORAGE_DRIVER: vfs
script:
- buildah bud -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- buildah push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
Buildah is OCI-compliant and daemonless. Good for teams already in the Red Hat ecosystem.
Quick Decision Table
| Approach | Privileged? | Speed | Complexity | Best For |
|---|---|---|---|---|
| DinD | Yes | Fast | Medium | Full Docker compatibility needed |
| Kaniko | No | Moderate | Low | Simple image builds |
| Buildah | No | Moderate | Low | Red Hat/OCI ecosystem |
| Docker socket binding | Yes (effectively) | Fastest | Low | Trusted environments only |
These errors look different every time but the root cause is one of three things. Tools like Daxtack pattern-match against known failure signatures like this automatically, which is faster than eyeballing it — but the checklist above works fine manually too.