Getting a “permission denied” error when a container tries to read or write to a mounted volume is one of the most common friction points when running Docker on Linux. It almost always comes down to a mismatch between the user ID running inside the container and the ownership of the files on the host, rather than any actual bug in Docker itself.
The Problem
You mount a host directory into a container using -v or a volumes entry in docker-compose.yml, and the application inside the container fails with errors like:
EACCES: permission denied, open '/app/data/file.txt'
PermissionError: [Errno 13] Permission denied: '/app/data'
The files exist, the path is correct, the volume mount syntax is fine, but the process inside the container simply can’t read or write to them, and it’s not obvious why since everything “looks” correctly configured.
Why It Happens
On Linux, Docker does not translate user IDs between the host and the container. Files on the host are owned by a specific UID/GID (for example, your user account, often UID 1000), while the process inside the container often runs as a different user — commonly root (UID 0) or a service-specific user created in the image (which might be UID 1001, 999, or something else entirely, depending on the base image and how its Dockerfile was written).
When the container’s process UID doesn’t match the UID that owns the files on the host, the Linux kernel enforces normal file permission rules and blocks access — Docker itself has no special exception for this, since from the kernel’s point of view, containers are just regular processes with regular permission checks applied to them like anything else on the system.
The Fix
First, check which user the container is actually running as, and compare it to the file ownership on the host:
docker compose exec app id
ls -lan /path/on/host
If the UIDs don’t match, you have a few options, from simplest to most correct depending on your setup:
Option 1: Match the container’s user to your host user. Pass your host UID/GID when running the container, so any files it creates match your own user, and existing files you already own remain readable:
docker run --user $(id -u):$(id -g) -v ./data:/app/data your-image
In docker-compose.yml:
services:
app:
user: "${UID}:${GID}"
volumes:
- ./data:/app/data
Option 2: Fix ownership on the host directory to match the UID the container expects. This is useful when the image runs as a fixed, non-configurable user that you don’t control:
sudo chown -R 1000:1000 /path/on/host
Option 3: Adjust permissions inside the Dockerfile, if you control the image, by creating the user with a UID that matches your deployment environment and setting proper ownership during the build itself, rather than patching it after the fact:
RUN useradd -u 1000 -m appuser
USER appuser
Still Not Working?
If you’re running Docker with SELinux enabled (common on Fedora, RHEL, and CentOS), permission errors can persist even after fixing UID/GID ownership, because SELinux applies its own labeling rules to mounted volumes independently of standard Unix permissions. In that case, add the :z or :Z suffix to the volume mount to let Docker relabel the content automatically:
docker run -v ./data:/app/data:Z your-image
Use :Z for a private, unshared volume and :z if the same volume is meant to be shared across multiple containers. Mixing these up won’t cause an error immediately, but can lead to confusing permission problems later when a second container tries to access the same volume with a conflicting label.
It’s also worth checking whether the issue only appears after switching machines or restoring a backup. Copying files between different Linux systems, or restoring from a tarball created on another host, often preserves the original UID/GID numbers rather than the actual usernames — so a directory that looked fine on the old machine can end up owned by a UID that doesn’t correspond to any real user on the new one. Running ls -lan (with the -n flag to show numeric IDs instead of names) makes this kind of mismatch immediately visible, even when ls -l alone shows a seemingly normal-looking owner.