Fixing containerd.sock Permission Denied in Rootless Docker: A Complete Debugging Guide
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Rootless Docker cannot access
containerd.sockbecause the socket is owned by root or lives outside the user'sXDG_RUNTIME_DIR, making everydockercommand fail withpermission denied. - How to fix it: Ensure
dockerd-rootless-setuptool.shran correctly for your user,XDG_RUNTIME_DIRis exported, and the systemd user service is active under the correct UID. - Shortcut: Use our Client-Side Sandbox below to auto-refactor your broken Docker daemon config or systemd unit file.
The Incident (What does the error mean?)
Raw error output from a broken rootless Docker environment:
$ docker ps
Cannot connect to the Docker daemon at unix:///run/user/1001/docker.sock. Is the docker daemon running?
$ docker info
ERROR: Got permission denied while trying to connect to the Docker daemon socket at
unix:///run/containerd/containerd.sock: Get "http://%2Frun%2Fcontainerd%2Fcontainerd.sock/v1.24/info":
dial unix /run/containerd/containerd.sock: connect: permission denied
Immediate consequence: Every Docker command is dead. No containers start. No images pull. If this is a CI runner, your entire pipeline is blocked. The daemon is either not running under your user context, or the DOCKER_HOST environment variable is pointing to the system-wide socket at /run/containerd/containerd.sock instead of the user-scoped socket at /run/user/$UID/docker.sock.
The Attack Vector / Blast Radius
This is not just an annoyance — it is a security boundary failure in both directions:
Scenario A — User escalating to root socket accidentally:
If a developer works around this by running sudo chmod 777 /run/containerd/containerd.sock or sudo usermod -aG docker $USER, they have re-introduced root-equivalent Docker access, completely defeating the purpose of rootless mode. Any process running as that user can now mount host filesystems, escape namespaces, and own the node. This is a textbook container escape prerequisite.
Scenario B — CI/CD runner misconfiguration:
In a shared CI environment (GitLab Runner, GitHub Actions self-hosted), if DOCKER_HOST is not scoped per-runner UID, one job's containerd socket bleeds into another user's namespace. A malicious pipeline job can attempt to connect to a neighbor's socket, enumerate running containers, and exfiltrate environment variables injected as Docker secrets.
Blast radius: Full host compromise if the workaround is chmod on the system socket. Data exfiltration and cross-tenant container inspection in shared CI environments.
How to Fix It (The Solution)
Step 1: Verify the actual socket path and daemon status
# Check if rootless daemon is running under YOUR user
systemctl --user status docker
# Confirm the correct socket path
echo $XDG_RUNTIME_DIR
ls -la /run/user/$(id -u)/docker.sock
If systemctl --user status docker shows inactive or failed, the rootless daemon never started. If XDG_RUNTIME_DIR is empty, the environment is broken.
Basic Fix: Re-run rootless setup and export environment
- # Missing or wrong DOCKER_HOST pointing to system socket
- export DOCKER_HOST=unix:///run/containerd/containerd.sock
+ # Correct: point to the user-scoped rootless socket
+ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
+ export XDG_RUNTIME_DIR=/run/user/$(id -u)
Add both exports to your ~/.bashrc or ~/.profile. Then enable and start the user-scoped service:
dockerд-rootless-setuptool.sh install
systemctl --user enable docker
systemctl --user start docker
# Enable lingering so daemon survives logout
sudo loginctl enable-linger $(whoami)
Enterprise Best Practice: Lock the socket path in the systemd user unit
If you are deploying rootless Docker across multiple users on a shared node (CI runners, multi-tenant build servers), do not rely on shell exports. Encode the socket path into the systemd user unit override:
# File: ~/.config/systemd/user/docker.service.d/override.conf
[Service]
- # No explicit socket path — inherits broken system default
- Environment=""
+ Environment="XDG_RUNTIME_DIR=/run/user/%U"
+ Environment="DOCKER_HOST=unix:///run/user/%U/docker.sock"
After writing the override:
systemctl --user daemon-reload
systemctl --user restart docker
This ensures the socket path is deterministic and UID-scoped regardless of how the shell session was initiated (SSH, cron, CI agent fork).
Verify UID namespace mapping is intact
# Must return a valid subordinate UID range
grep $(whoami) /etc/subuid
grep $(whoami) /etc/subgid
# Expected output:
# youruser:100000:65536
If these entries are missing, rootless mode cannot create user namespaces and containerd will refuse to start:
- # /etc/subuid — missing entry for your user
+ youruser:100000:65536
# After editing /etc/subuid and /etc/subgid
dockerд-rootless-setuptool.sh install
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Enforce socket path in CI runner configuration (GitLab example):
# .gitlab-ci.yml
variables:
- DOCKER_HOST: "unix:///var/run/docker.sock" # system socket — WRONG for rootless
+ DOCKER_HOST: "unix:///run/user/1001/docker.sock" # UID-scoped rootless socket
2. OPA/Conftest policy to block system socket references in CI configs:
package ciconfig
deny[msg] {
input.variables.DOCKER_HOST == "unix:///var/run/docker.sock"
msg := "DOCKER_HOST must not reference the system socket. Use rootless user-scoped socket."
}
3. Checkov custom check for Dockerfile and compose files:
Add a pre-commit hook that greps for /var/run/docker.sock volume mounts in docker-compose.yml files — binding the system socket into a container is an immediate critical finding in any rootless deployment.
# .pre-commit-config.yaml
- repo: local
hooks:
- id: block-system-docker-sock
name: Block system docker.sock bind mounts
entry: bash -c 'grep -r "/var/run/docker.sock" . && exit 1 || exit 0'
language: system
pass_filenames: false
4. Ansible/Terraform provisioning: When bootstrapping nodes, always include loginctl enable-linger for service accounts running rootless Docker. Without linger, the user daemon dies on logout and the socket disappears, causing the exact permission denied error on the next CI job.