The Docker CLI is present and nothing is listening on the socket it is trying. In CI this is almost never a broken daemon; it is that no daemon was provided.
docker version 2>&1 | head -20
echo "DOCKER_HOST=${DOCKER_HOST:-unset}"
Client: Docker Engine - Community
Version: 27.3.1
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Client present, server absent. That is the whole diagnosis.
GitLab: docker-in-docker
build:
image: docker:27-cli
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_SHA" .
All four variables matter. This is the configuration people get half right:
| Variable | Why |
|---|---|
DOCKER_HOST | Port 2376 is TLS, 2375 is plain. dind defaults to TLS |
DOCKER_TLS_CERTDIR | Where dind writes certificates. Empty string disables TLS |
DOCKER_CERT_PATH | Where the client reads them |
DOCKER_TLS_VERIFY | Tells the client to use TLS |
Setting DOCKER_HOST to 2376 without the certificate variables gives a connection error even though dind is running perfectly.
The hostname is docker, from the service image name. no such host when resolving docker means the service did not start; check the job log's service section.
Match the client and dind versions. A client much newer than the server produces client version is too new, and pinning both to the same major avoids it.
dind requires a privileged runner:
# /etc/gitlab-runner/config.toml
[[runners]]
[runners.docker]
privileged = true
volumes = ["/certs/client", "/cache"]
/certs/client must be in volumes or the client cannot read the certificates the service wrote.
The socket-binding alternative
[[runners]]
[runners.docker]
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
Faster, since the layer cache persists between jobs. And it gives every job root on the runner host: a job can mount the host filesystem, read other jobs' data, and start privileged containers. On a shared runner that is not acceptable. It is defensible on a single-tenant runner you control.
Build without a daemon
Where privileged mode is unavailable, and for anything multi-tenant, daemonless builders are the better answer.
Kaniko:
build:
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "$CI_PROJECT_DIR"
--dockerfile "$CI_PROJECT_DIR/Dockerfile"
--destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
Kaniko reads credentials from /kaniko/.docker/config.json, which GitLab populates automatically for its own registry.
Buildah:
script:
- buildah bud -t "$IMAGE" .
- buildah push "$IMAGE"
Both produce standard OCI images and need no privileged container. Kaniko's layer caching is weaker than BuildKit's, which is the main practical cost.
GitHub Actions
Hosted runners have Docker running already, so this error usually means one of:
A container job. With container:, your steps run inside it, and that container has no Docker CLI unless the image provides one.
jobs:
build:
runs-on: ubuntu-latest
container: node:22 # no docker CLI inside
Drop the container: and use setup-node, or mount the socket:
container:
image: node:22
volumes:
- /var/run/docker.sock:/var/run/docker.sock
A macOS runner. No Docker daemon at all; macOS runners are virtualised and cannot run Linux containers. Use ubuntu-latest for anything Docker.
A self-hosted runner where Docker is not installed, or the runner user is not in the docker group:
sudo usermod -aG docker "$USER" # log out and back in
Prefer the official build action, which sets up BuildKit properly:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/acme/app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
type=gha uses the Actions cache for layers, which is the single biggest speed improvement available here.
Kubernetes executors
A GitLab runner on Kubernetes needs the dind service as a sidecar, and privileged containers are frequently blocked by Pod Security Admission. This is the situation where Kaniko or Buildah is not merely preferable but necessary.
A checklist
docker version. Client without server means no daemon was provided.echo $DOCKER_HOST. It decides where the client looks.- GitLab dind → all four variables, and
docker:NN-dindas a service. - Runner needs
privileged = trueand/certs/clientinvolumes. - Match client and dind major versions.
- Socket binding is faster and gives every job root on the host.
- No privileged mode → Kaniko or Buildah.
- GitHub Actions → check for a
container:job or a macOS runner.
Frequently Asked Questions
Why does GitLab's docker-in-docker need four variables?
Because the daemon runs in a separate container reachable over the network rather than a local socket, and modern dind enables TLS by default. DOCKER_HOST points at tcp://docker:2376, the TLS port. DOCKER_TLS_CERTDIR tells dind where to write its certificates, DOCKER_CERT_PATH tells the client where to read them, and DOCKER_TLS_VERIFY turns client TLS on. Setting the host without the certificate variables is the common half-configuration, and it produces a connection error that looks like the service never started.
Is mounting the Docker socket into CI jobs safe?
Not on shared infrastructure. A job with access to the host's Docker socket can start a privileged container, mount the host filesystem and read every other job's data, which is effectively root on the runner. It is faster than docker-in-docker because the layer cache persists between jobs, and that is a reasonable trade on a single-tenant runner you control entirely. On anything multi-tenant, use a daemonless builder instead.
How do I build images without a Docker daemon?
Kaniko and Buildah both build standard OCI images in an unprivileged container. Kaniko runs as a job image with its own entrypoint and pushes directly to a registry; Buildah offers a Dockerfile-compatible bud command. Neither needs a privileged runner, which matters where Pod Security Admission or platform policy forbids it, as is common on Kubernetes-based runners. The main cost is weaker layer caching than BuildKit provides.
Why does Docker work on my GitHub runner but not inside my job?
Because the job specifies a container:, so your steps execute inside that image rather than on the runner host, and the image almost certainly has no Docker CLI or access to the host's socket. Either remove the container: directive and use a setup-* action for your language, or mount the socket into the container with a volumes entry. The other possibility is a macOS runner, which has no Docker daemon at all and cannot run Linux containers.
What does "client version is too new" mean?
The Docker CLI is negotiating an API version the daemon does not support, which happens when the client image is a newer major version than the dind service. Pin both to the same major, for example docker:27-cli alongside docker:27-dind, rather than using latest for either. Setting DOCKER_API_VERSION to the server's version also works as a stopgap, though matching the images is cleaner and avoids the problem recurring on the next image refresh.