Docker permission denied docker.sock on a CI runner
Docker permission denied docker.sock means the daemon is running and the user your step runs as is not allowed to open the unix socket it listens on. Every Docker command in the job fails identically and instantly, including ones that do nothing, because none of them ever reached the daemon to be refused by it.

What this error means
The failure is total and immediate: docker ps, docker version and docker build all produce the same line, and they produce it in the same fraction of a second, because the refusal happens at the socket rather than anywhere inside Docker. It appears on self-hosted runners whose service account was never added to the docker group, on container jobs that bind-mount the socket, and after a group change that nothing has picked up yet. A tail of the message names an HTTP request against a percent-encoded socket path, which makes it look like a network problem and is not one.
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.40/build?...: dial unix /var/run/docker.sock: connect: permission deniedThe word "Got" tells you how old your Docker is
This message has been reworded twice, and which version you have is a free reading of the client on the runner. The literal lives in the Docker Go API client, in moby client/request.go, which the CLI and every Go tool that talks to the daemon compile in. From v1.13.1 through v20.10.23 it began with a capital Got. From v20.10.24 through v28.5.2 the Got is gone and the rest is identical. On moby master the wording changed again, to a lowercase reference to the docker API rather than to the daemon socket.
So a log that starts with Got is a client at v20.10.23 or older, which is a strong hint on its own: it usually means an old self-hosted runner image rather than a hosted one. The GitHub-hosted ubuntu-22.04 and ubuntu-24.04 images both listed Docker 28.0.4 when we read them on 2026-09-21, so a hosted Linux runner prints the form without Got.
| Docker client version | How the socket refusal reads |
|---|---|
| v1.13.1 through v20.10.23 | Got permission denied while trying to connect to the Docker daemon socket at ... |
| v20.10.24 through v28.5.2 | permission denied while trying to connect to the Docker daemon socket at ... |
| moby master, after v28.5.2 | permission denied while trying to connect to the docker API at ... |
Common causes
The runner service account is not in the docker group
The ordinary cause on a self-hosted runner. The machine was provisioned, Docker was installed, and the account the runner agent runs as was never added to the group that owns the socket. Every Docker step has failed since the runner was registered, which is a useful signature: this cause does not appear suddenly.
The group was added but nothing restarted
A process keeps the group list it was given when it started, so the fix was applied and the running runner agent never saw it. In our experience this is the cause when somebody has already run usermod, confirmed the membership with id in an interactive shell, and the pipeline still fails.
A container job mounts the socket and runs as another user
Bind-mounting the socket into a container carries the file and its numeric group, not the host group name. If the container user is not in a group with that numeric id, it cannot open the socket, and no amount of configuration on the host will change it.
The socket ownership or mode was changed
Hardening scripts and hand-written unit overrides sometimes alter the socket group or drop the group write bit. This is rarer than the others and shows up as a socket whose listing does not match a stock install, which is why listing the socket is worth doing before anything else.
How to fix it
Add the runner account to the docker group, then restart the runner
- Add the account the runner agent actually runs as, which may not be your login.
- Restart the runner service so the agent picks up the new group list.
- Confirm from inside a job with
id -nG, not from an interactive shell.
sudo usermod -aG docker svc_runner
sudo systemctl restart actions.runner.myorg-myrepo.runner01.service
# then, in a workflow step
- run: id -nGPass the socket group numerically into container jobs
Read the numeric group of the socket on the host and give it to the container with --group-add, because the group name inside the container is almost never the same. This keeps the container running as its normal user instead of solving the problem by running it as root.
docker run --group-add "$(stat -c '%g' /var/run/docker.sock)" \
-v /var/run/docker.sock:/var/run/docker.sock \
myorg/ci-tools:1 docker psCheck the socket itself before changing any user
List the socket and compare it against a stock install: root owner, docker group, group writable. If the listing is different, the fix is the socket rather than the account, and changing group membership will not help no matter how many times it is applied.
ls -l /var/run/docker.sock
stat -c '%U %G %a' /var/run/docker.sockSay what you need in the job, rather than escalating
Prefixing every Docker command with sudo works and gives the whole step root, which is more than it needed and hides the real configuration problem from whoever reads the workflow next. Fix the group once on the host and keep the steps unprivileged.
- name: Confirm socket access before the Docker steps
run: docker version --format '{{.Server.Version}}'Nothing in the daemon decided this
It is worth being precise about who refused you, because it changes where you look. The daemon never saw the request. The client tried to open a unix socket, the kernel checked the file mode against the calling user, and the open failed; the client then recognized a permission error and replaced the raw failure with a readable sentence. Restarting the daemon therefore does nothing, and neither does anything inside daemon.json.
What decides it is ordinary filesystem permission on one file. The socket is owned by root, its group is docker on a normal install, and it is group writable, so the question is only whether the user running your step is in that group at the moment the process started.
# the three facts that settle it
ls -l /var/run/docker.sock
id
id -nG | tr ' ' '\n' | grep -x docker || echo "not in the docker group"Group membership is decided when the process starts
The reason this error survives the obvious fix is that adding a user to a group does not change any process that is already running. The runner service inherited its groups when it started, so usermod -aG docker fixes the next login and not the job currently failing, and not the runner agent that spawned it.
On a self-hosted runner, add the group and then restart the runner service, in that order. Inside a container job that bind-mounts the socket, there is no login at all, so the container has to be given the socket group numerically at start time instead.
# self-hosted runner host
sudo usermod -aG docker "$(whoami)"
sudo systemctl restart actions.runner.*.service
# a container that mounts the socket
docker run --group-add "$(stat -c '%g' /var/run/docker.sock)" \
-v /var/run/docker.sock:/var/run/docker.sock myorg/ci-tools:1What the runner does about it
No repair, and no recorded run, and on this page the reason is close to home: our runners are built so that the job user is already in the docker group, so producing this failure on one would mean removing that membership first. The capture would then be of a runner configured differently from the runners we ship, which tells you nothing about either. The failure belongs to self-hosted setups and to socket-mounting containers, which is where the fixes above are aimed.
It is also not a failure anything should repair automatically. Granting a process access to the daemon socket is granting it root-equivalent control of the host, and a runner that quietly added a user to the docker group to make a red build go green would be making a security decision on your behalf. That one should stay manual.
How to prevent it
- Put the docker group membership in the runner provisioning, not in a manual step.
- Restart the runner service whenever the service account gains a group.
- Give socket-mounting containers the numeric socket group at start time.
- Run
docker versionas the first Docker step so access is proven before real work starts.
Frequently asked questions
Why does the error start with "Got" in some logs and not others?
client/request.go began with a capital Got from v1.13.1 through v20.10.23, and from v20.10.24 onward the word is gone. Nothing else about the sentence changed, so a log that still says Got is telling you the client on that machine is at least a few years old.I ran usermod and it still fails. Why?
Does this happen on GitHub-hosted runners?
Is adding a user to the docker group safe?
Related guides
References
- moby v28.5.2: the socket permission literal, client/request.go
- moby v20.10.23: the same line with the older "Got" wording
- samber/github-actions-runner#1: the line quoted above, reported on a runner
- Docker docs: post-installation steps, managing Docker as a non-root user
- Docker documentation
- Docker build cache
- GitHub Actions documentation