Fixing Docker volume permission denied on bind mounts
A practical, least-invasive fix for docker volume permission denied on bind mounts by aligning container UID/GID with host directory ownership, with Linux and Windows steps.
Rao Aadil, India 10 min read
What 'docker volume permission denied' actually means
A bind mount maps a host directory directly into a container. The container process runs with a numeric user ID (UID) and group ID (GID) on Linux, or a Windows SID on Windows containers. That identity must have write permission on the host directory for the container to create, modify, or delete files there. If it does not, the kernel returns Permission denied. The application inside the container usually reports EACCES or mkdir: cannot create directory.
Named volumes behave differently. A named volume is managed by Docker, stored under a Docker-controlled directory such as /var/lib/docker/volumes/ on Linux, and Docker normally creates the volume with permissions that match the container's root user from the image. Bind mounts inherit the host directory's ownership and ACLs as-is, so the container user must match the host owner or have group/other write access.
Common error signatures include Permission denied in application logs, EACCES in stack traces, mkdir: cannot create directory '/data/cache': Permission denied, or an application reporting a read-only file system when the mount is actually read-write. The same container often works with a named volume but fails with a bind mount.
Inspect containers, read logs, check stats, restart services and run Compose stacks, just by describing what you want.
“why does this container keep restarting”
It reads the real state of the server before it says anything, shows you the exact command, and waits for your approval. Windows and Linux, over the SSH access you already have.
Step 1: Reproduce the failure and confirm it is a permissions problem
Run a minimal container with the same bind mount and attempt a write. On Linux:
docker run --rm -v /host/data:/data alpine sh -c 'touch /data/test'
On Windows PowerShell with Docker Desktop using Linux containers, the equivalent is:
docker run --rm -v C:\data:/data alpine sh -c 'touch /data/test'
Docker Desktop translates C:\data to /run/desktop/mnt/host/c/data inside the Linux VM, but the permission check still uses the host NTFS ACLs translated through the WSL2 layer. If the command fails with Permission denied, you have confirmed the mount is the problem.
Check the application's own logs for a more specific path or operation:
docker logs <container>
Verify the mount configuration and read-write mode with docker inspect:
docker inspect -f '{{ json .Mounts }}' <container>
Look for the Source (host path), Destination (container path), and RW: true. A bind mount that is read-only will also cause permission errors, but the fix is different: remove the :ro suffix or read_only: true from your Compose file.
Enter the container and try writing to the exact path that fails. On Linux containers:
docker exec -it <container> sh
Then run touch /data/test or whatever your app tries. On Windows native containers, use:
docker exec -it <container> cmd
Then try echo test > C:\data\test.txt. This confirms the failing path inside the container.
Step 2: Compare the container's user with the host directory ownership
On Linux, get the container's UID and GID:
docker exec <container> id
Inspect the host directory ownership:
ls -l /host/data
stat -c '%u %g' /host/data
The output of stat shows the numeric owner UID and group GID. For example, 1000 1000 means the host directory is owned by UID 1000 and GID 1000.
On Windows with Docker Desktop and WSL2, open a WSL terminal and run id inside the container or the WSL distro to see the numeric UID/GID that Docker applies to Linux containers. To see how the Windows directory appears inside the WSL Linux environment, run:
ls -l /mnt/c/data
Or use Docker Desktop's internal mount path:
ls -l /run/desktop/mnt/host/c/data
The numeric UID/GID shown here corresponds to the Windows user that owns the files. Compare that with the container's UID.
On Windows native containers, get the container's Windows identity:
docker exec <container> whoami
Inspect the host directory ACL:
icacls C:\data
The output lists accounts and their rights. Common mismatches include:
- Host directory owned by UID 1000 but the container process runs as UID 0 (root).
- Host owned by root but the container runs as UID 1000.
- On Windows, the NTFS ACL grants
Readbut notWriteorModifyto the container's SID or account.
Least-invasive fix: run the container as the host directory owner
Run the container process with the same UID and GID that owns the host directory. Add --user to docker run:
docker run --user 1000:1000 -v /host/data:/data your-image
In a Docker Compose file, set user: on the service:
services:
app:
image: your-image
user: "1000:1000"
volumes:
- /host/data:/data
Use the numeric UID/GID from Step 2. Numeric values avoid ambiguity when the container image does not have the same username mapping as the host.
On Windows with Docker Desktop and Linux containers, the same numeric UID/GID applies. On Windows native containers, use a Windows account such as ContainerUser:
docker run --user ContainerUser -v C:\data:C:\data your-windows-image
Then ensure the host directory ACL grants that account write permission (see the next section).
If the application needs to create files as multiple users, add the container user to a shared group and use --group-add with that group's GID:
docker run --user 1000:1001 --group-add 1000 -v /host/data:/data your-image
The host directory should be group-writable by that group.
Alternative fix: adjust the host directory permissions
Changing host permissions is more invasive because it affects all processes on the host, not just the container. Prefer running the container as the right user whenever possible. If that is not feasible, adjust the directory ownership or mode.
On Linux, change the directory owner to match the container UID/GID:
chown 1000:1000 /host/data
Or add group write permission if the container shares a group with the host:
chmod g+w /host/data
Avoid chmod 777 because it grants write access to every user on the system and often masks a UID mismatch that will reappear on another host.
On Windows with WSL2, you have two filesystem views. For directories under the Linux filesystem (for example, inside \\wsl$\<distro>\...), use chown or chmod from the WSL distro:
chown 1000:1000 /path/to/data
For directories on the Windows filesystem mounted under /mnt/c, the ownership is controlled by Windows NTFS ACLs. Use icacls from an elevated PowerShell on the Windows side:
icacls C:\data /grant <WindowsUser>:(OI)(CI)F
Replace <WindowsUser> with the Windows account that owns the directory.
On Windows native containers, if the container runs as ContainerUser, grant the container's SID modify rights. The well-known SID for ContainerUser is S-1-5-93-2-2, so:
icacls C:\data /grant *S-1-5-93-2-2:(OI)(CI)M
Verify the container's identity with whoami first; the exact account or SID depends on your Windows container configuration. Grant the minimum rights needed: M (modify) is usually sufficient.
When to switch to named volumes
Named volumes avoid the host UID/GID mismatch entirely. Docker creates the volume and manages its permissions. The volume is stored in Docker's own directory, not directly exposed as a host path unless you use a bind mount to the volume's location.
Create and use a named volume:
docker volume create appdata
docker run -v appdata:/data your-image
In Compose:
services:
app:
image: your-image
volumes:
- appdata:/data
volumes:
appdata:
To migrate existing data from a bind mount to a named volume, run a temporary container that copies files:
docker run --rm -v /host/data:/source -v appdata:/dest alpine cp -a /source/. /dest/
Use a named volume when you need persistent application data but do not need direct host access to the files. This is the recommended approach for databases, uploads, and other stateful data, and it eliminates the docker volume permission denied class of problems.
Special case: SELinux and AppArmor on Linux
On SELinux-enforcing hosts such as RHEL, CentOS, or Fedora, a bind-mounted directory may be denied even when UID/GID match because SELinux requires a security label. Add :z (shared, multiple containers) or :Z (private, single container) to the bind mount:
docker run -v /host/data:/data:z your-image
z relabels the directory so multiple containers can access it. Z relabels it as private to one container. Use :Z only when you are sure no other container needs access, because it changes the label in a way that other containers cannot read.
AppArmor can also deny writes if a profile restricts mount access. Check for denials:
dmesg | grep denied
aa-status
If a profile is the cause, adjust the profile or, for testing only, run with --security-opt apparmor=unconfined. Do not use unconfined in production.
Windows does not have SELinux or AppArmor, but Windows Defender or third-party antivirus can sometimes block writes to a bind mount. If all other causes are ruled out, check Windows security logs for blocked file events.
Rootless Docker and user namespace mapping
Rootless Docker runs the daemon as a non-root user and maps container UIDs to subordinate UID ranges defined in /etc/subuid and /etc/subgid. A host directory owned by your user may appear as root inside the container, or vice versa, because of the namespace mapping.
Check the mapping by running a container that prints its user and comparing to the host:
docker run --rm alpine id
id
If the UID inside the container differs unexpectedly, either set the container user to the mapped UID or consider --userns=host if you understand the security implications. --userns=host disables the user namespace for that container and is generally not recommended.
On Windows, rootless Docker is not the common setup. Docker Desktop uses WSL2 and its own user namespace rules, so focus on WSL2 ownership and NTFS permissions as described earlier.
Prevent recurrence: set users and volumes explicitly
Define a non-root user in the Dockerfile and document the UID/GID:
RUN groupadd -g 1000 app && useradd -u 1000 -g app app
USER app
Always set user: in Compose for services that bind mount host directories. Keep the UID/GID in sync with the host by using build args or environment variables:
services:
app:
build: .
user: "${APP_UID:-1000}:${APP_GID:-1000}"
volumes:
- /host/data:/data
Add a CI check that runs a write test as the container user against a test bind mount. For example:
docker run --rm -u 1000:1000 -v /host/data:/data alpine touch /data/test
Fail the build if the command returns Permission denied. Keep a consistent base image and avoid switching the default UID unexpectedly across updates.
Using an AI agent to shorten the diagnosis
An AI agent that manages Docker can confirm the symptom by reading container logs and running id inside the container to show the UID/GID the container is using. It can inspect your Compose file and compare the configured user against the host directory ownership. Ask it “why does this container keep restarting” or “what user is this container running as?” The agent can run read-only diagnostics inside the container to identify the failing path. It cannot modify host file permissions or run chown/chmod on the host, so you must apply the host-side fix manually. After you adjust the permissions or the --user flag, ask the agent to restart the service and read the new logs to confirm the error is gone.