Docker Error Guide: 'cannot create directory: Permission denied' — Volume & Bind-Mount Ownership
Fix Docker's 'cannot create directory: Permission denied' on volumes and bind mounts caused by UID/GID mismatches between the container user and host directory ownership.
- #docker
- #troubleshooting
- #errors
- #permissions
Overview
A container fails to write into a mounted directory when the process’s user does not have permission on the host-owned path. It appears in container logs and interactive runs:
mkdir: cannot create directory '/data/cache': Permission denied
The shell-builtin variant looks the same with a leading sh::
sh: cannot create directory '/data/cache': Permission denied
Bind mounts and named volumes carry host filesystem ownership (UID/GID) straight into the container. If the container runs as a user whose UID does not match the directory’s owner and the mode does not grant write access, every create/write returns EACCES. This is an ownership/permissions mismatch, not a Docker bug.
Symptoms
- A container that writes to a mounted path crashes at startup with
Permission denied. - The same image works with no mount but fails once a bind mount or volume is attached.
- Running the image as root works, but running as a non-root user fails.
- Files created inside the container appear on the host owned by an unexpected UID (or vice versa).
- A fresh named volume works, but a bind mount to a host directory owned by a different user does not.
Common Root Causes
- UID/GID mismatch — the container process runs as a user (e.g. UID 1000 or an app-specific UID) that does not own the host directory.
- Non-root
USERin the image writing to a root-owned mount. - Bind-mount source owned by the host user with a restrictive mode (e.g.
700) the container UID cannot enter. - Read-only mount (
:ro) where the app expects to write. - SELinux context on the host blocking container access to the bind mount (needs
:z/:Z). - Root-created files on a named volume that a later non-root container cannot modify.
Diagnostic Workflow
First, identify which user the container runs as and which UID that maps to:
docker run --rm myapp:1.4.2 id
Check the ownership and mode of the mount target inside a throwaway container:
docker run --rm -v /data/myapp:/data alpine ls -ld /data
Compare that against the host directory’s ownership:
ls -ld /data/myapp
If the container UID (from id) does not match the directory owner and the group/other bits do not grant write, that is the mismatch. On SELinux hosts, check whether the context is the blocker:
ls -Z /data/myapp
Confirm the fix works by aligning ownership and re-running. Chown the host directory to the container’s UID/GID:
sudo chown -R 1000:1000 /data/myapp
docker run --rm -v /data/myapp:/data myapp:1.4.2 sh -c 'mkdir -p /data/cache && echo ok'
Example Root Cause Analysis
An image runs its app as a non-root user baked in as USER app (UID 1000). The container mounts a host directory and crashes on start:
mkdir: cannot create directory '/data/cache': Permission denied
Checking the container user and the host directory reveals the mismatch:
docker run --rm myapp:1.4.2 id
# uid=1000(app) gid=1000(app)
ls -ld /data/myapp
# drwxr-xr-x 2 root root 4096 Jul 5 10:12 /data/myapp
The directory is owned by root:root with mode 755, so UID 1000 can read but not write. The container user cannot create /data/cache. The fix is to align the host directory ownership with the container’s UID/GID:
sudo chown -R 1000:1000 /data/myapp
Re-running the container now succeeds because the app user owns the mount. On an SELinux host, the mount would also need the :Z flag so Docker relabels the directory for the container:
docker run -d -v /data/myapp:/data:Z myapp:1.4.2
An alternative that avoids host chowning is to run the container with a matching --user "$(id -u):$(id -g)" so it writes as the host owner.
Prevention Best Practices
- Decide on a fixed application UID/GID and provision host directories with matching ownership during setup.
- Use
--user "$(id -u):$(id -g)"for bind mounts when you want files owned by the host user. - Prefer named volumes for app data; initialize their ownership with a one-time
chownin an entrypoint or init container. - On SELinux systems, mount bind sources with
:z(shared) or:Z(private) so Docker applies the correct label. - Avoid running production containers as root just to sidestep permissions; fix ownership instead.
- Validate Dockerfiles and their
USER/WORKDIRchoices with the Dockerfile validator before shipping.
Quick Command Reference
# Which user/UID does the container run as?
docker run --rm myapp:1.4.2 id
# Ownership/mode of the mount as the container sees it
docker run --rm -v /data/myapp:/data alpine ls -ld /data
# Host directory ownership
ls -ld /data/myapp
# Align host ownership to the container UID/GID
sudo chown -R 1000:1000 /data/myapp
# Run as the host user instead of chowning
docker run --user "$(id -u):$(id -g)" -v /data/myapp:/data myapp:1.4.2
# SELinux relabel on mount
docker run -v /data/myapp:/data:Z myapp:1.4.2
Conclusion
cannot create directory: Permission denied on a volume or bind mount is an ownership mismatch: the container’s process UID does not have write permission on the host-owned path. Diagnose it by comparing docker run --rm myapp id against ls -ld of the mount, then either chown the host directory to the container’s UID/GID, run the container as the host user with --user, or apply the SELinux :Z flag. Standardizing a fixed app UID and provisioning directory ownership up front prevents it. For more volume and permission fixes, see the Docker guides.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.