Skip to main content
Systems Engineering

GitHub Actions Docker Build: Fixing Permission Denied on docker.sock

Diagnose docker.sock permission denied in GitHub Actions by runner type, then apply a least-privilege fix with validation and rollback.

Black boots with blue striped socks resting on sand, showcasing summer beach vibes.

In this guide

Share

#Context

The error reads: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock. It means the process running the step cannot open the Docker daemon socket. The documented general causes are that the user lacks access to the socket, or that no daemon is reachable from where the step runs. The fix depends on where the job executes: a GitHub-hosted runner, a container job or a self-hosted runner. On a self-hosted runner, the usual fix is to grant the runner service user access to the daemon, restart the service, and accept that this access is root-equivalent on that host. Runner image contents and container job behaviour are documented in the GitHub Actions documentation.

Docker failures in a pipeline fall into three distinct classes that are often confused because they all surface as a failed build step. A socket permission fault is a local operating system access problem. A registry authentication fault is an identity and scope problem that appears at push or pull time. A caching problem is a performance problem, not a correctness problem. Treating them separately keeps the diagnosis short and prevents widening access when the real fault is a missing login step.

Related reading: bin-packing CI jobs across runner fleets covers runner capacity and placement, and ephemeral preview environments shows a build-and-deploy pattern that depends on working image builds.

#
Assumptions

  • You can read the workflow file and, for self-hosted runners, have shell access to the runner host.
  • You are validating in a non-production repository on a non-production runner before changing a shared one.
  • The runner host is Linux. Windows and macOS runners expose Docker differently and are out of scope here.

#Architecture

The Docker command line is only a client. It sends API requests to a daemon, and on Linux the default transport is a Unix domain socket at /var/run/docker.sock. That socket is a filesystem object with an owner, a group and mode bits, so ordinary file permission rules decide who may connect. By default the socket is owned by root with group docker, so a user needs either root or membership of that group. The error text is therefore an operating system permission result, not a GitHub Actions policy decision.

Three execution contexts change what the client can see.

Where a Docker step runs and what usually decides access
ContextWhat the step seesTypical cause of the errorFirst check
GitHub-hosted runnerA virtual machine provisioned for the job with the runner image toolingRare; usually a step running inside a container or a wrong assumption about image contentsCheck runs-on and any container: key
Container jobA container with its own filesystem, possibly without a Docker client or the host socketSocket not present in the container, client missing, or the container user lacks accessLook for container: at job level and any mounted volumes
Self-hosted runnerThe host as the runner service userService user is not in the docker group, or the group change has not been applied to the running serviceRun id for the service user and list the socket

On a GitHub-hosted runner, Docker is generally provided on the Linux virtual machine, so the socket is normally usable by the job user without extra setup. In a container job the step runs inside a container, so unless a client and the socket are provided inside it, the command either is missing or cannot reach a daemon. Self-hosted runners are the most common source of the permission error, because the runner application runs as a service user that an administrator created and that may never have been granted daemon access.

One consequence of this design deserves emphasis. Access to the daemon socket is not a narrow privilege. A client that can talk to the daemon can ask it to start containers with host filesystem mounts and elevated settings, so granting socket access is equivalent to granting root on that host. This shapes both the fix and the security posture covered below.

Shipping containers and cranes at Hamburg port showcasing global trade.
Photo by Wolfgang Weiser on Pexels

#Implementation

Work through the contexts in order, and change nothing until you know which one you are in.

  1. Identify the context. Read the workflow runs-on value. A label such as an Ubuntu image name indicates a GitHub-hosted runner, while custom labels or self-hosted indicate your own host. Then check for a job-level container: key, because that moves the steps inside a container regardless of where the runner lives.
  2. Capture the exact message. The wording distinguishes the fault. permission denied on the socket is an access problem. Cannot connect to the Docker daemon ... Is the docker daemon running? points to a stopped daemon or a missing socket. docker: command not found points to a missing client. unauthorized or denied: requested access to the resource is denied at push time points to registry authentication.
  3. Self-hosted: inspect the service user. Find which user the runner service runs as, then check its groups and the socket ownership using the read-only commands below.
  4. Container job: decide whether the host daemon is needed. Often a dedicated build action or a build step on a hosted runner is safer than mounting the host socket into a job container. If the job genuinely needs the daemon, provide it deliberately and understand that the container then has host-level reach.
  5. Registry login. Authenticate in the workflow before pushing, using repository secrets rather than literal credentials, and grant the job only the permissions it needs. A login failure is not fixed by changing socket access.
  6. Caching. Treat the build cache as an optimisation. A cache miss makes a build slower; it does not cause a permission error, so do not change access to chase a slow build.

Read-only diagnostic commands on the runner host:

1id runner
2ls -l /var/run/docker.sock
3docker version

Expected evidence: the socket is listed with owner root and group docker. The id output includes docker among the groups only if access has been granted. docker version prints both a Client and a Server section when access works. If access is missing, the Client section prints, followed by the same permission denied message as the failing job.

Substitute the real service account name for runner. If the account name is unknown, inspect the runner service definition on the host rather than guessing, because adding the wrong account widens access for no benefit.

The state-changing fix on a self-hosted runner is adding the service user to the docker group:

1sudo usermod -aG docker runner

Group membership is read when a process starts, so the running runner service keeps its old groups until it is restarted. Stop the runner service, start it again, and then repeat the diagnostic commands as that user. Do this on one non-production runner first, and take it out of rotation while you change it so no job is mid-flight during the restart. Do not use broad permission changes such as making the socket world-writable; that removes the boundary for every user on the host and is not a supportable fix.

#Validation

Success is observable at three levels, and each should pass before you declare the fault fixed.

  1. Host level. id for the service user lists the docker group, and docker version run as that user prints a Server section and exits zero.
  2. Job level. A minimal workflow in a non-production repository, targeting only the changed runner, runs docker info and exits zero. This proves the runner process, not only an interactive shell, holds the new group.
  3. Pipeline level. The real build runs in the non-production repository. Confirm the image builds, and if the pipeline pushes, confirm the push succeeds with the intended credentials and that the resulting image is present in the registry.

Also validate negative cases. A repository that should not be able to use the runner must still be unable to target it, and a job on a different runner must be unaffected. If an interactive check passes but the job still fails, the usual explanation is that the service was not restarted after the group change, or that the job runs in a container that never received the socket.

#Failure modes

  • Group change made but the error persists. The runner service was not restarted, so it keeps its old group list. Restart the service and retest at job level.
  • Cannot connect to the daemon, with no permission wording. The daemon is stopped, or the job runs in a container without the socket. Check the daemon state on the host and the container: key in the workflow.
  • Command not found. The step runs in a container image without a Docker client. Use an image that includes a client or move the build to a context that has one.
  • Unauthorized on push. Registry login is missing, the secret is wrong, or the token lacks write scope. Fix the login step and the token scope; do not alter socket access.
  • Slow rebuilds. The cache is not being restored or its key changes on every run. This is a performance fault only.
  • Fix works on one runner but not another. Runners in a fleet were provisioned differently. Compare group membership and service configuration across hosts, and consider building the fix into the image or provisioning code rather than patching hosts by hand.
Vibrant red and blue shipping containers under a clear sky, perfect for industrial themes.
Photo by Jan van der Wolf on Pexels

#Security

Docker socket access is equivalent to root on the host, and the security assessment should start there. A workflow that can reach the socket can start privileged containers, mount host paths and read anything the host holds, including credentials left by earlier jobs on a persistent runner. Treat the decision to grant it as a privilege decision, not a convenience setting.

  • Prefer GitHub-hosted runners or ephemeral self-hosted runners, so that each job starts from a clean host and a compromised job does not persist.
  • Do not share a self-hosted runner that has socket access with untrusted code, such as workflows triggered by pull requests from forks.
  • Do not mount the socket into containers that run untrusted workloads.
  • Keep registry credentials in repository or environment secrets, scoped to the minimum needed, and avoid printing them in logs.
  • Record who approved the access change and why, so that it can be reviewed and reversed.

Residual risk remains even after a correct fix: the runner is still a high-value target. The aim is to limit which repositories can reach it and how long it lives. General operational guidance on safe deployment, automation and observability is described in the Microsoft Learn operational excellence design principles, and a gated release approach is covered in deterministic rollout gating for progressive delivery.

#Recovery

Treat the group change as a reversible change with a defined stop point. Stop and roll back if any of these occur: jobs from repositories that should not use the runner begin running on it, the runner fails to come back online after restart, or unexpected containers appear on the host.

  1. Take the runner out of rotation so no new jobs are scheduled to it.
  2. Remove the service user from the docker group.
  3. Restart the runner service so the removal takes effect, then confirm with id that the group is gone.
  4. Confirm that the original permission denied error returns for the validation workflow. This is a safe failure state: builds fail closed rather than run with unreviewed access.
  5. Review the change, then decide whether to retry, move to ephemeral runners or use a hosted runner.

No images or data are removed by this rollback, so no backup is needed for the access change itself.

#Operational checks after a Docker access fix

Once the fix is in place, make it part of routine operation rather than a one-off repair.

  • Confirm the runner shows as online and idle, and that the validation workflow still passes after the next host update or image rebuild.
  • Confirm only the intended repositories can target the runner by reviewing runner group and repository access settings.
  • Check the host for unexpected running containers after a job completes.
  • Watch for a rise in failed Docker steps, and classify each by message as socket, daemon, client, authentication or cache, so repeat faults point to a provisioning gap.
  • Capture the group membership step in provisioning code so a rebuilt runner does not reintroduce the original error.

The next safe decision depends on what you found. If broad socket access is not acceptable for the repositories involved, move those builds to GitHub-hosted or ephemeral runners rather than widening access on a persistent host. If the fault was authentication or caching, leave socket access unchanged and correct the workflow instead.

Evidence trail

Sources and verification

Primary documentation and external technical references used in this article.

  1. 01GitHub Actions documentationdocs.github.com
  2. 02operational excellence design principleslearn.microsoft.com

About this page

Credited to

Dates

Published

Corrections

No corrections are listed for this page. Some review sweeps corrected pages without naming each one; those are in the corrections log.

Reader Interaction

Comments

Add a thoughtful note on GitHub Actions Docker Build: Fixing Permission Denied on docker.sock. Comments are checked for spam and held for moderation before appearing.

Loading comments...

Discover more

Learn More About KBY

Was this useful?

Reports go to the site owner and are used to correct pages. Corrections are listed at /corrections.

Engineering insights, direct to you.

Receive the latest Systems Engineering tutorials, production guides, Engineering Labs and operational best practices.