The fast fix
If your GitLab CI job fails with Cannot connect to the Docker daemon at unix:///var/run/docker.sock, your docker client is looking for a local socket that does not exist inside the job container, because DOCKER_HOST is not set. Point the client at the docker:dind service over TCP and the error goes away:
build:
image: docker:28.3
services:
- name: docker:28.3-dind
alias: docker
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 my-app .
Enter fullscreen mode Exit fullscreen mode
That is the whole fix for the common case. The rest of this page explains why the socket variant of the error is different from the tcp://docker:2375 variant, and covers the two other setups (socket-mounted runners and the Kubernetes executor) where the same message shows up for a different reason.
Why you get the unix socket variant specifically
This error is not the same as Cannot connect to the Docker daemon at tcp://docker:2375. The address in the message tells you exactly what the client tried:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Enter fullscreen mode Exit fullscreen mode
When DOCKER_HOST is empty, the Docker CLI falls back to its compiled-in default, the local unix socket at /var/run/docker.sock. Inside a GitLab CI job that uses the docker executor, that socket file simply is not there. The daemon runs in a separate docker:dind service container, not in your job container, so there is nothing listening on the local socket. The client connects, finds no socket, and prints the message above.
The tcp://docker:2375 form is the opposite problem: DOCKER_HOST is set correctly but the dind service is not reachable (missing service, no privileged mode, or a TLS mismatch). If you are seeing that address instead, read the companion write-up on the tcp://docker:2375 form of this error, which walks the service and privileged-mode causes in detail. This page is about the case where the client never even tried the network, because nothing told it to.
The three real causes
1. DOCKER_HOST is unset
This is the usual cause. You added services: - docker:dind but never set DOCKER_HOST, so the client ignores the service container and hits the local socket. Set it as a job or top-level variable:
variables:
DOCKER_HOST: tcp://docker:2376
Enter fullscreen mode Exit fullscreen mode
Use port 2376 (TLS) when DOCKER_TLS_CERTDIR is set, and 2375 (plain) when you disable TLS with DOCKER_TLS_CERTDIR: "". Mixing them is the second most common mistake, covered next.
2. TLS is half-configured
Docker Engine 19.03 and later turns on TLS between the client and the daemon by default. The dind service generates certificates into the path named by DOCKER_TLS_CERTDIR. If you set the certs directory but point DOCKER_HOST at the plain-text port 2375, or you set neither cert variable, the handshake fails and the client can end up falling back to the socket. Keep the three TLS variables consistent:
variables:
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_CERT_PATH: "/certs/client"
DOCKER_TLS_VERIFY: "1"
Enter fullscreen mode Exit fullscreen mode
If you would rather skip TLS for an internal runner, disable it cleanly and use port 2375:
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
Enter fullscreen mode Exit fullscreen mode
Pick one style and set every variable it needs. Do not leave DOCKER_TLS_CERTDIR set while talking to 2375.
3. The runner cannot start dind at all
If DOCKER_HOST is right but the dind container never boots, the client still fails, sometimes reporting the socket address after a retry. The docker:dind service needs privileged mode in the runner’s config.toml:
[[runners]]
executor = "docker"
[runners.docker]
privileged = true
Enter fullscreen mode Exit fullscreen mode
With privileged = false or the key absent, dind cannot create its own daemon and no address will work. Confirm this on the runner host before touching your pipeline file.
Socket-mounted runners are the exception
Some self-managed runners deliberately mount the host’s Docker socket instead of running dind. In that setup /var/run/docker.sock is supposed to exist inside the job, and the same error means the mount is missing or the path is wrong. The runner’s config.toml binds the host socket:
[[runners]]
executor = "docker"
[runners.docker]
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
Enter fullscreen mode Exit fullscreen mode
With this style you do not set DOCKER_HOST at all, because the local socket is the daemon. If you get the unix socket error here, check that the host actually has Docker running and that the bind path in volumes matches the real socket location. This approach shares the host daemon with every job, so treat it as a security tradeoff, not a default. The dind service is the safer choice for untrusted pipelines.
The Kubernetes executor needs the same variables
Running GitLab Runner on Kubernetes does not change the fix, but it adds one gotcha. Each dind service runs as a sidecar container in the same Pod, so DOCKER_HOST: tcp://docker:2376 still resolves through the service alias. What breaks people is TLS cert sharing between containers in the Pod. Set an explicit shared volume for the certs directory in the runner’s Helm values or pin DOCKER_TLS_CERTDIR to a path both containers can read. If certs land in a directory only the dind container sees, the client falls back to the socket and you get this exact error inside Kubernetes.
Verify the fix
Add a one-line probe to the top of your job and rerun the pipeline:
$ docker info --format '{{.ServerVersion}}'
Enter fullscreen mode Exit fullscreen mode
If that prints a version string, the client reached the daemon and your docker build will work. If it still fails, echo the variable to confirm the pipeline actually applied it:
$ echo "DOCKER_HOST=$DOCKER_HOST"
Enter fullscreen mode Exit fullscreen mode
An empty value here means your variables block is scoped wrong (defined under the wrong job, or shadowed by a group or project variable). Move it to the top level or the specific job that runs Docker.
Prevention checklist
- Always set
DOCKER_HOSTexplicitly when you usedocker:dind. Never rely on the default socket in a CI job. - Keep the TLS variables consistent:
2376with a cert dir, or2375withDOCKER_TLS_CERTDIR: "". Never mix them. - Pin the image and the dind service to the same tag, for example
docker:28.3anddocker:28.3-dind, so client and daemon versions match. - Confirm
privileged = truein the runnerconfig.tomlbefore pushing jobs that need dind. - On Kubernetes, give the certs directory a shared volume so both containers in the Pod can read it.
For the daemon flags behind all of this, including the -H host option and the default unix:///var/run/docker.sock binding, see the Docker daemon reference. Once your builds connect reliably, the multi-stage build guide is a good next step for shrinking the images those pipelines produce.
답글 남기기