GitLab CI Error Guide: 'Cannot connect to the Docker daemon at tcp://docker:2375' Docker-in-Docker
Fix GitLab dind 'Cannot connect to the Docker daemon': add the docker:dind service, set DOCKER_HOST/DOCKER_TLS_CERTDIR for TLS on 2376, and enable privileged mode.
- #gitlab-cicd
- #troubleshooting
- #errors
- #docker-in-docker
Exact Error Message
A Docker-in-Docker (dind) job fails the moment it runs a docker command:
$ docker info
Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?
ERROR: Job failed: exit code 1
The socket variant appears when no DOCKER_HOST is set and the client falls back to a local socket that does not exist in the build container:
$ docker build -t app .
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
The error comes from the Docker client inside your build container, not from GitLab. The client looked for a daemon at the address shown and found nothing listening.
What the Error Means
A dind job runs two containers that GitLab links together: your build container (with the Docker client, e.g. image: docker:27) and a service container running the Docker daemon (docker:27-dind). The build container talks to the daemon over the network, reaching the service by its alias hostname docker.
The error means the client could not reach that daemon. Three things must all line up:
- The
docker:dindservice is actually declared and running, so something listens on thedockerhost. DOCKER_HOSTpoints at the right port and protocol —tcp://docker:2375for plaintext,tcp://docker:2376for TLS. Modern dind enables TLS by default and listens on 2376, so a job still hard-coded to 2375 connects to a closed port.- The runner allows privileged mode, because dind needs it to run a nested daemon. Without it the daemon never starts, so even a correct address has nothing to connect to.
If any one of those is wrong, the client reports Cannot connect to the Docker daemon.
Common Causes
- No
docker:dindservice declared in the job, so nothing serves thedockerhost. - TLS/port mismatch. dind defaults to TLS on 2376 with
DOCKER_TLS_CERTDIR=/certs, but the job setsDOCKER_HOST=tcp://docker:2375(plaintext port) — or vice versa. - Privileged mode not enabled on the runner. The dind daemon cannot start without
privileged = true; the service container exits and the hostdockeranswers nothing. DOCKER_HOSTunset, so the client falls back tounix:///var/run/docker.sock, which does not exist in the build container.- Service alias / networking issues, e.g. a non-default
FF_NETWORK_PER_BUILDsetting or a custom alias the client does not use. - Overlay/MTU problems on certain hosts requiring
--mtuor theoverlay2storage driver. - Version default drift. Newer GitLab/dind images turned TLS on by default; configs written for older images break after an image bump.
How to Reproduce the Error
Run a Docker command without a dind service, or with the wrong port:
# .gitlab-ci.yml — broken: no dind service, plaintext port
build-image:
image: docker:27
variables:
DOCKER_HOST: "tcp://docker:2375" # nothing listening here
script:
- docker info # -> Cannot connect to the Docker daemon at tcp://docker:2375
Even adding the service is not enough if the runner lacks privileged mode — the docker:27-dind daemon fails to start, and the same error appears on a port that should be open.
Diagnostic Commands
Start by reading the job log: the address in the error tells you the port/protocol the client tried. Then confirm the service, the variables, and the runner’s privileged flag:
# In the job script, before failing, dump what the client is targeting
docker version # shows client OK, server unreachable
echo "DOCKER_HOST=$DOCKER_HOST CERTDIR=$DOCKER_TLS_CERTDIR"
# Probe whether anything is listening on the dind ports from the build container
( apk add --no-cache curl >/dev/null 2>&1 || true )
curl -s -m 3 http://docker:2375/_ping || echo "2375 closed"
curl -sk -m 3 https://docker:2376/_ping || echo "2376 closed"
On the runner host, confirm privileged mode and the executor:
# /etc/gitlab-runner/config.toml — privileged is REQUIRED for dind
[[runners]]
executor = "docker"
[runners.docker]
image = "docker:27"
privileged = true
volumes = ["/certs/client"]
# Verify the runner config and service health
sudo grep -A8 '\[runners.docker\]' /etc/gitlab-runner/config.toml
sudo journalctl -u gitlab-runner -n 50 --no-pager
docker info # on the host, sanity check the host daemon (Kubernetes uses a dind service instead)
Set variables: { CI_DEBUG_TRACE: "true" } in the job to see the full environment GitLab injects, which confirms whether DOCKER_HOST/DOCKER_TLS_CERTDIR are what you expect.
Step-by-Step Resolution
-
Declare the dind service and match its TLS port. The modern, recommended setup uses TLS on 2376:
build-image: image: docker:27 services: - docker:27-dind variables: DOCKER_HOST: "tcp://docker:2376" DOCKER_TLS_CERTDIR: "/certs" DOCKER_CERT_PATH: "/certs/client" DOCKER_TLS_VERIFY: "1" script: - docker info - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . -
Enable privileged mode on the runner (
privileged = trueinconfig.toml) and mount/certs/clientso the client finds the generated certs. Restart:sudo gitlab-runner restart. -
If you must use plaintext (not recommended), disable TLS consistently on both sides:
variables: DOCKER_HOST: "tcp://docker:2375" DOCKER_TLS_CERTDIR: "" # empty disables TLS; dind then listens on 2375 -
Keep client and dind image tags aligned (
docker:27+docker:27-dind) so TLS defaults match. -
For overlay/MTU issues, pass
command: ["--mtu=1400"]to the dind service or set theoverlay2storage driver. -
On the Kubernetes executor, the
services:dind container needs privileged too — setprivileged = trueunder[runners.kubernetes]. -
Re-run the job;
docker infoshould now report a reachable server.
Prevention and Best Practices
- Use TLS on 2376 with
DOCKER_TLS_CERTDIR=/certs— the secure default — and mount/certs/client. Reserve plaintext 2375 for throwaway local testing only. - Pin matching image tags for the client and the dind service so a base-image bump cannot silently flip TLS defaults and break the port.
- Centralize dind variables in a hidden template job and
extends:it, so every image-building job uses the same correctDOCKER_HOST/TLS settings. - Treat
privileged = trueas a security decision. Dedicate a tagged runner to dind jobs rather than enabling privileged mode fleet-wide, and consider rootless or Kaniko/Buildah where privileged is unacceptable. - Validate with
docker infoas the first script line so a connectivity problem fails fast with a clear message instead of mid-build. - For triage, the free incident assistant can read a dind connection error and suggest the port/TLS fix. More patterns live in the GitLab CI/CD guides.
Related Errors
- GitLab CI Error: Kubernetes pod timed out — when the dind service container itself will not start on Kubernetes.
- GitLab CI Error: stuck runners tag mismatch — get the job onto a privileged-capable runner in the first place.
- GitLab CI Error: Invalid CI config — when the
services:block itself is malformed.
Frequently Asked Questions
Why 2376 and not 2375? Modern docker:*-dind images enable TLS by default and listen on 2376, generating certs into DOCKER_TLS_CERTDIR. Port 2375 is the plaintext endpoint and is closed when TLS is on. A job still pointing at 2375 connects to a dead port and gets Cannot connect to the Docker daemon.
Do I really need privileged mode? For classic dind, yes — the nested daemon needs it to start. If your security policy forbids privileged runners, use a daemonless image builder like Kaniko or Buildah instead of dind.
Why does it say unix:///var/run/docker.sock? That is the Docker client’s default when DOCKER_HOST is unset. There is no Docker socket inside the build container, so the client fails. Set DOCKER_HOST=tcp://docker:2376 (or mount the host socket, which is usually discouraged).
It worked last month and now it’s broken — what changed? Almost always an image tag bump. Moving from an older docker:dind to a newer one flips TLS on by default and switches the listening port to 2376. Pin tags and update DOCKER_HOST/DOCKER_TLS_CERTDIR together.
Does this apply to the Kubernetes executor? Yes. You still declare docker:dind as a service, still use TLS on 2376, and still need privileged = true — but set it under [runners.kubernetes] in config.toml rather than [runners.docker].
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.