How to Fix File Permissions on Docker Bind Mounts Safely
Fix permission denied on Docker bind mounts by matching user IDs, using the user option, fixing ownership, and handling SELinux labels and rootless mode.

"Permission denied" on a mounted directory, or files owned by root that you cannot delete, is the signature problem of bind mounts. The container and the host share files but not user databases, and the filesystem only knows numbers. This post finds the mismatch and offers four fixes in order of preference. It continues from volumes and bind mounts. The commands assume Docker Engine 27 and Debian 13.
Look at the numbers
id -u
docker run --rm -v "$PWD":/w -w /w alpine:3.20 sh -c 'id; ls -ln'1000
uid=0(root) gid=0(root) groups=0(root)
-rw-r--r-- 1 1000 1000 120 Nov 3 10:02 app.py
The files are owned by UID 1000 on the host, and the container runs as root (UID 0) by default. Root can read them, so nothing fails here, but anything root creates will be owned by 0 on the host. If the image runs as a non-root user, for example UID 999 for a database, that user cannot write to files owned by 1000.
Fix 1: run as your own user
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/w -w /w node:22 npm ciThe tool writes files as you, so the host sees ordinary ownership. In Compose, set user: "${UID}:${GID}" and export those variables, since the shell does not export UID by default. Some images need a writable home directory for the arbitrary user; set -e HOME=/tmp when a tool complains.
Fix 2: change the directory's owner
sudo chown -R 999:999 ./pgdata
sudo chmod -R u+rwX ./pgdataWhen the image's user is fixed, such as 999 for the official Postgres image, give that number the directory. The host user will need sudo to touch those files, which is why this is better for data than for source code. A named volume avoids it altogether, because Docker initializes the volume with the image's ownership.
Fix 3: SELinux labels
docker run --rm -v "$PWD":/w:z alpine:3.20 ls /wOn Fedora, RHEL and similar systems, SELinux denies container access to files with the wrong label, and the error looks like a normal permission failure even when the mode is 777. :z relabels the directory for shared use by several containers; :Z labels it for this one container only. Never use either on a system directory such as /home or /etc, because relabeling it can break the host.
Fix 4: rootless Docker and user namespaces
With rootless Docker or user-namespace remapping, container root maps to an unprivileged host UID, often 100000 and up. A file owned by your UID 1000 appears as owned by nobody inside. Use podman unshare chown or docker run --user 0:0 with the rootless mapping in mind, or avoid the issue with a named volume. Check the mapping in /etc/subuid.
Where this does not apply
Docker Desktop for Mac and Windows presents shared files with permissive ownership, so these errors rarely appear there and then show up in Linux CI. Test the same command on Linux before assuming it works. For hardening, avoid chmod 777 as a cure; it removes the protection for every user on the host. See running as a non-root user and debugging permissions with namei.
Why the numbers rather than the names matter
The kernel stores file ownership as numbers, and names exist only because of lookup files such as /etc/passwd inside each environment. A container has its own copy of that file, so the user called app may be number 999 in one image and 1001 in another, and neither equals your login on the host. When a bind mount exposes a host directory, the container compares the numbers on the files with the number its process runs as. There is no translation, which is why ls -ln on both sides is the first command to run. If the directory is owned by 1000 and the process runs as 33, the mode bits decide whether it can write, and a typical 755 directory refuses it.
Root inside the container is root on the host unless user namespaces are in use, so a process running as root can write anywhere the host mode allows root to, and files it creates are owned by root on the host. That is why a container that runs as root fills a project directory with files you then cannot delete without sudo.
Choosing the fix that fits
There are four honest fixes and they suit different situations. Running the container as your own numeric user, with --user "$(id -u):$(id -g)" or the user key in Compose, is right for development, since files stay yours on both sides. Changing the directory's owner or mode with chown or chmod on the host is right when the container's user is fixed by the image and the data directory is dedicated to it. Using a named volume instead of a bind mount is right when you do not need to touch the files from the host, since Docker handles ownership with the image's own layout. Granting access through a group, by adding a supplementary group with group_add and setting the directory group-writable, is right when several users must share a directory. Avoid the shortcut of chmod 777, which fixes the symptom by removing all protection and makes the files writable by any process on the host.
Distribution specific causes
On hosts with SELinux enforcing, such as Fedora and the Red Hat family, a permission error can occur even when the numbers and modes are right, because the container's process type is not allowed to access files labelled for the host. The suffix :z on the bind mount relabels the content for sharing between containers, and :Z relabels for exclusive use by one. Never apply those labels to a system directory such as your home folder or /usr, because relabeling a broad path can break the host. The audit log, read with ausearch -m avc, confirms that SELinux is the cause and not a plain permission problem.
Rootless Docker and Podman map container users into a range of subordinate IDs, so container root is an unprivileged number on the host. A file you see as owned by your user on the host may appear as root inside, and the reverse. Check /etc/subuid and the output of podman unshare ls -ln, which shows ownership as the container sees it, and set ownership from inside that namespace so the numbers line up.
A short procedure for the next error
When the message says permission denied, work through it in the same order each time. Print the numbers on both sides with ls -ldn on the host path and docker exec ctr id and ls -ldn inside. Compare the owner, the group and the mode against the user the process runs as. Check for SELinux with getenforce and for a read-only flag in the mount definition. Check that the directory exists on the host, since a missing one is created as root. Fix the cause with the narrowest change that works and write it into the Compose file or Dockerfile so it survives the next rebuild.
The most durable fix is in the image: create the user with a fixed numeric ID, create the data directory owned by that ID, and document the number so hosts can match it. Teams that publish images with a stable user ID save every consumer from rediscovering the same problem. Entrypoint scripts that adjust ownership at start-up with chown -R are common and cost startup time proportional to the number of files, and they run as root, so drop privileges afterwards.
See the bind mount SELinux notes and rootless mode.


