DEV Community

SelfHost Pilot
SelfHost Pilot

Posted on Originally published at selfhostpilot.com

Docker permission denied: why it happens and how to fix it properly

Docker permission denied: what the error is actually telling you

A client messages me about this error almost every week. They run a command, see "permission denied", and assume Docker is broken.

Docker is rarely broken. People just use one name for two completely different errors.

The first happens before any container starts: your user cannot talk to the Docker daemon. The second happens inside a running container: the app cannot write a file.

The fixes have nothing in common, so tell them apart first. Below are the four causes I see in client work.

What "docker permission denied" means in one sentence

Docker permission denied means the process that tried to write a file (or talk to the daemon) is not the user that owns the file (or the socket), and it has no group or other rights to it either.

The kernel does not care about names like www-data or hitesh. It compares numbers: the UID and GID of the process against the owner and mode bits of the file.

Socket error File error
What you see permission denied ... docker.sock Permission denied or EACCES in container logs
Who actually failed Your shell user, running the docker CLI The app process inside the container
Where to look /var/run/docker.sock and your groups Host path owner, container user, SELinux

Cause 1: you cannot reach the Docker daemon socket

This is the one you see on a fresh server:

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

The Docker daemon always runs as root and listens on /var/run/docker.sock, owned by root:docker with mode 660. If you are not root or in the docker group, the CLI cannot open it.

Check both sides first:

`ls -l /var/run/docker.sock

srw-rw---- 1 root docker 0 Oct 7 09:12 /var/run/docker.sock

id -nG

hitesh adm sudo <- no "docker" here, that is the problem`

Then the fix:

`sudo groupadd docker # fine if it says the group already exists
sudo usermod -aG docker $USER

log out and back in, or for the current shell only:

newgrp docker
docker run --rm hello-world`

Group membership is read at login, so old shells lack it. newgrp docker fixes the current shell only. I once lost 20 minutes to a tmux pane that never got the change.

The ~/.docker/config.json trap

If you ran sudo docker ... before fixing the group, you may see this:

WARNING: Error loading config file: /home/hitesh/.docker/config.json - stat /home/hitesh/.docker/config.json: permission denied

The sudo run created ~/.docker as root, so your own user cannot read it. Give it back:

sudo chown "$USER":"$USER" "$HOME/.docker" -R
sudo chmod g+rwx "$HOME/.docker" -R

The honest warning

Docker's own documentation warns that the docker group grants root-level privileges. Anyone in it can run docker run -v /:/host -it alpine chroot /host and get a root shell on the host.

That is fine on my single-user VPS. On a shared box, it is not a safe substitute for sudo.

Cause 2: a root-owned bind mount and a non-root container

The most common file error: you mount ./data into a container running as UID 1000, the host directory belongs to root, and the app exits with EACCES: permission denied.

Usually Docker created it: if the host path is missing and you use the short volume syntax, Docker creates it as root. It is on my list of first-container mistakes for that reason.

Three commands show the mismatch:

`id

uid=1000(hitesh) gid=1000(hitesh)

ls -ln /srv/app

drwxr-xr-x 2 0 0 4096 Oct 7 10:02 data

docker compose exec app id

uid=1000(node) gid=1000(node)`

Use ls -ln, not ls -l, because the kernel compares numbers, not names. Here the process is 1000 and the directory is 0 with mode 755, so writes fail.

Make the numbers match: chown the directory to the container's UID, or run the container as your host user:

`services:
app:
image: myapp:latest
user: "1000:1000"
volumes:

  • ./data:/data`

Community images like the linuxserver.io ones use PUID and PGID environment variables for the same job. That is a convention, not a Docker feature, so it only works on images that read it.

` environment:

  • PUID=1000
  • PGID=1000`

Order matters. Create the directory with the right owner before the first start, or you end up cleaning a root-owned folder the image half-initialised:

mkdir -p /srv/app/data
sudo chown 1000:1000 /srv/app/data
docker compose up -d

When rootless mode or userns-remap is on

Both modes shift UIDs. In rootless mode, container UID 0 maps to the host user running the daemon, which is why your own files show as root inside. Container UID n (n of 1 or more) maps to subuid + (n - 1): with hitesh:100000:65536 in /etc/subuid, UID 1000 becomes 100999.

With userns-remap, container UID 0 maps to the first subordinate UID and UID n to subuid + n, so with the same 100000 start, UID 1000 becomes 101000. GIDs follow /etc/subgid the same way. Chown to the mapped number.

Cause 3: the app writes at runtime into a directory it does not own

This one passes every check at startup and fails later. My clearest case was FreePBX 17 in Docker. After a batch of module installs, the admin panel loaded with broken JavaScript and the log showed:

file_put_contents(/var/www/html/admin/assets/js/pbxlib_...js): Failed to open stream: Permission denied

The installs had run as root through docker exec, so the files they created were root-owned. Apache in that image runs as the asterisk user, so /var/www/html/admin/assets/js was not writable when it tried to regenerate the bundle.

who owns the directory, and who is Apache?

docker compose exec freepbx ls -ld /var/www/html/admin/assets/js
docker compose exec freepbx ps -eo user,comm | grep apache2

fix ownership the FreePBX way

docker compose exec freepbx fwconsole chown
docker compose exec freepbx fwconsole reload

fwconsole chown left the tree as asterisk:asterisk with mode 775, and the error went away.

That fix is only correct because Apache in that image runs as asterisk, not www-data. On an image where Apache runs as www-data, the same chown locks the web server out.

My rule since: run installs and upgrades as the app's user, or run the app's own ownership tool straight after.

Cause 4: SELinux on Fedora, RHEL and CentOS hosts

Sometimes owner and mode are perfect and the write still fails. On Fedora, RHEL, CentOS, Rocky or Alma, check SELinux next.

`getenforce

Enforcing

ls -ld /srv/app/data

drwxr-xr-x. 2 1000 1000 6 Oct 7 11:40 /srv/app/data

ls -ldZ /srv/app/data

drwxr-xr-x. 2 1000 1000 unconfined_u:object_r:var_t:s0 6 Oct 7 11:40 /srv/app/data

sudo ausearch -m avc -ts recent

avc: denied { write } for comm="node" name="data"

scontext=system_u:system_r:container_t:s0:c12,c345

tcontext=unconfined_u:object_r:var_t:s0 tclass=dir`

The trailing dot after the mode bits means an SELinux context, which ls -lZ shows. The avc: denied line proves SELinux blocked the write, not Unix permissions. Containers run as container_t and can only write container-labelled files.

The fix is a volume suffix that relabels the path:

  • :z relabels the content so several containers can share it.

  • :Z relabels it for exclusive use by that one container.

` volumes:

  • ./data:/data:Z`

The relabel happens on first use. Never put :Z on /home or /etc, because it relabels the host's own files too.

setenforce 0 also hides the error. That is a workaround, not a fix: it disables enforcement for the whole host until the next reboot, when your error returns.

Why chmod 777 is the wrong fix

Every forum thread has someone suggesting chmod -R 777. It makes the error go away, which is exactly the problem.

  • It hides the cause. The owner is still wrong, and you never learn which user the app runs as.

  • It breaks the app's security assumptions. Nextcloud keeps its database password in a config file, and 777 lets any user on the host read and edit it.

  • It does not last. The next container or upgrade writes new files with its own owner and mode, and the problem returns.

The order I check them in

  • Read the full error text, not your memory of it.

  • Decide socket or file. If it mentions docker.sock, it is Cause 1.

  • Check who owns the path on the host with ls -ln.

  • Check which user the process runs as inside with docker compose exec app id. If the numbers differ, it is Cause 2 or 3.

  • If the numbers match and the host is enforcing SELinux, run ausearch -m avc. That is Cause 4.

If your error is "connection refused" or a timeout between containers, that is not a permission problem, and my Docker networking guide covers it.

Frequently asked questions

Is it safe to add my user to the docker group?

On a single-admin server, yes, if you treat that user as root, because it effectively is. On a shared machine, use sudo or rootless Docker.

Why does the file appear as root-owned on my host after the container writes it?

The container process ran as UID 0, and without rootless mode or userns-remap, UID 0 inside is UID 0 on the host. Set user: in compose, or PUID and PGID if the image supports them.

Does the same fix work on Docker Desktop for Mac and Windows?

Mostly no. Docker Desktop runs the daemon in its own Linux VM, handles socket access itself, and translates ownership on shared folders. Inside the WSL 2 filesystem on Windows, normal Linux rules and the Cause 2 fix apply.

This article was written with AI assistance and reviewed by a human. The FreePBX case was reproduced in a lab on a throwaway VM; the other command output shown is typical output from a Debian 12 machine, trimmed to the lines that matter.

If you would rather hand this off: I fix self-hosted stacks for a living, and a permission problem is usually a 30-minute job.


This article was written with the help of AI (our own publishing automation) and reviewed by a human before posting.

Top comments (0)