The fast fix
If your GitLab CI job dies with error during connect: ... dial tcp: lookup docker on 127.0.0.11:53: no such host, your docker client resolved DOCKER_HOST fine but the DNS name docker does not exist on the job's network. That name is the alias of the docker:dind service, and it only registers when the service container actually starts. The usual fix is to define the service with the -dind image tag and an explicit alias, and point DOCKER_HOST at TLS port 2376:
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 .
That covers the common case on the docker executor. If you are on the Kubernetes executor, or the service still refuses to resolve, keep reading. The name resolution is the whole game here, and there are three distinct reasons it fails.
Why this error is not the daemon-connection error
The address in the message tells you exactly how far the client got. This DNS variant is different from the two connection variants:
error during connect: Post "https://docker:2376/v1.44/info":
dial tcp: lookup docker on 127.0.0.11:53: no such host
The client tried to resolve the hostname docker through the container DNS resolver (127.0.0.11 on a Docker bridge network) and got back nothing. That is a name-resolution failure, not a refused connection. Compare it to the tcp://docker:2375 form, where the name resolves but the daemon is unreachable, covered in the Docker daemon connection error write-up, and the unix:///var/run/docker.sock form, where DOCKER_HOST was never set at all, covered in the socket-variant fix. If your message says no such host, the client never reached any daemon because the name pointed at nothing.
The three real causes
1. The dind service never started, so its alias never registered
This is the most common cause and the least obvious. GitLab registers the docker alias on the build network only when the docker:dind service container comes up. If that container fails to start, the alias is missing and every lookup returns no such host. Three things stop it from starting:
-
You used the plain image, not the
-dindtag.docker:28.3ships only the client. You needdocker:28.3-dind, which bundlesdockerd. A plaindockerservice starts, exits immediately (no daemon to run), and takes its alias down with it. -
The runner is not in privileged mode.
docker:dindruns its own daemon and needsprivileged = truein the runnerconfig.toml. Without it the container cannot start the daemon and dies during boot. -
The image failed to pull. A registry rate limit or a typo in the tag means the service container never exists. Check the job log's
Preparing the "docker" ...service lines near the top.
Pin the runner config and confirm privileged mode:
$ grep -A3 '\[runners.docker\]' /etc/gitlab-runner/config.toml
You want to see privileged = true. If it says false or is absent, that is your problem.
2. A renamed service image broke the derived alias
GitLab derives a service's default alias from its image name. docker:28.3-dind becomes the alias docker. But if you pull the image through a mirror or a private registry, the derived alias changes and docker stops resolving:
services:
# WRONG: alias becomes "my-mirror.example.com__docker", not "docker"
- name: my-mirror.example.com/library/docker:28.3-dind
The daemon is running, but under a name your DOCKER_HOST never asks for. Always set the alias explicitly when the image path is anything other than the bare docker name:
services:
- name: my-mirror.example.com/library/docker:28.3-dind
alias: docker
3. The Kubernetes executor puts services on localhost
On the docker executor, the service is a linked container with its own alias, so tcp://docker:2376 is correct. The Kubernetes executor is different: every service runs as a container in the same Pod as the build, sharing one network namespace. They all reach each other on localhost, and the docker alias may not resolve at all. If you copied a working docker-executor config onto a Kubernetes runner, this is why it broke.
Set the host to localhost for the Kubernetes executor:
variables:
DOCKER_HOST: tcp://localhost:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_CERT_PATH: "/certs/client"
DOCKER_TLS_VERIFY: "1"
Behavior here has shifted across runner versions, so verify against your own version rather than trusting a blog snippet. If tcp://localhost:2376 gives you connection refused instead of no such host, the name resolved and you are back to a daemon-startup problem (see cause 1). The two messages tell you which side of the wall you are on.
A checklist that isolates the cause in under a minute
Work through these in order the next time the job goes red:
-
Read the address in the error.
no such hostis DNS.connection refusedis a live-but-unreachable daemon. Do not fix the wrong one. -
Confirm the
-dindtag. Grep your.gitlab-ci.ymlfor the service image. No-dind, no daemon, no alias. -
Check privileged mode in the runner
config.tomlas shown above. -
Match
DOCKER_HOSTto your executor.tcp://docker:2376for thedockerexecutor,tcp://localhost:2376for Kubernetes. -
Match the port to TLS. With
DOCKER_TLS_CERTDIRset, the daemon listens on2376. Blank it out and it listens on the plain2375. A mismatch here surfaces as a connection error once the name resolves. -
Scan the top of the job log for the service
Preparing/Waitinglines. A service that logs an exit code never registered its alias.
Confirm the daemon is reachable before you build
Once the service is up and named correctly, prove the client can talk to it before your real build steps run. A one-line docker info at the start of script fails fast and loud instead of letting a ten-minute build collapse at the push step:
script:
- docker info
- docker build -t my-app .
- docker push my-app
The daemon's TCP listener and its 2375-versus-2376 split are documented in the Docker daemon reference; the TLS port only exists because DOCKER_TLS_CERTDIR generated certificates on boot. Keep the port and the TLS variables in sync and the no such host error stays gone. Once your images build cleanly, tightening them is the next job, and multi-stage builds are where that starts.
Top comments (0)