CI/CD

Cannot connect to the Docker daemon in a CI job

The job has a Docker client and no daemon. How docker-in-docker, socket binding and rootless builders differ, and which to choose.

medium fix5 min read

the ci/cd error
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

error during connect: Get "http://docker:2375/_ping": dial tcp: lookup docker on 10.0.0.2:53: no such host

ERROR: Preparation failed: Error response from daemon: client version 1.47 is too new

Cannot connect to the Docker daemon at tcp://docker:2376

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Establish whether a daemon exists at all

    docker version 2>&1 | head -20; echo "DOCKER_HOST=${DOCKER_HOST:-unset}"

    A client version printed with a server error means the CLI is installed and nothing is listening. DOCKER_HOST tells you where it is looking, which is the first thing to get right.

  2. 2

    For GitLab, attach the dind service and point the client at it

    echo 'services: [docker:27-dind]; variables: DOCKER_HOST=tcp://docker:2376 DOCKER_TLS_CERTDIR=/certs DOCKER_CERT_PATH=/certs/client DOCKER_TLS_VERIFY=1'

    The service runs the daemon in a sibling container reachable as the hostname docker. All four variables matter: omitting the TLS ones gives a connection refused on 2376 even though the service is running.

  3. 3

    If privileged mode is unavailable, build without a daemon

    echo "use kaniko or buildah, both build OCI images with no docker daemon"

    docker-in-docker requires a privileged runner, which many platforms and security policies forbid. Daemonless builders avoid the requirement entirely and are usually the better answer on shared infrastructure.

All 7 sections

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:

VariableWhy
DOCKER_HOSTPort 2376 is TLS, 2375 is plain. dind defaults to TLS
DOCKER_TLS_CERTDIRWhere dind writes certificates. Empty string disables TLS
DOCKER_CERT_PATHWhere the client reads them
DOCKER_TLS_VERIFYTells 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

  1. docker version. Client without server means no daemon was provided.
  2. echo $DOCKER_HOST. It decides where the client looks.
  3. GitLab dind → all four variables, and docker:NN-dind as a service.
  4. Runner needs privileged = true and /certs/client in volumes.
  5. Match client and dind major versions.
  6. Socket binding is faster and gives every job root on the host.
  7. No privileged mode → Kaniko or Buildah.
  8. 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.

Reference and practice

Learn the underlying concept

Other CI/CD errors