Skip to content

Fix: Docker Volume Permission Denied – Cannot Write to Mounted Volume

FixDevs ·

Quick Answer

How to fix Docker permission denied errors on mounted volumes caused by UID/GID mismatch, read-only mounts, or SELinux labels.

The Error

You mount a host directory into a Docker container and the application inside the container cannot write to it:

$ docker run -v /home/user/data:/app/data myapp
Error: EACCES: permission denied, open '/app/data/output.json'

Or you see it during a build or at container startup:

$ docker run -v ./logs:/var/log/myapp myapp
mkdir: cannot create directory '/var/log/myapp/archive': Permission denied

Or the container runs but silently fails to write, and you find this in the logs:

[error] failed to open '/app/data/config.yaml': Permission denied (os error 13)

The container can read the mount but cannot create, modify, or delete files inside it. Sometimes it cannot even read.

Why This Happens

When you mount a host directory into a container with -v or --mount, the files inside the container retain the same ownership and permissions they have on the host. Docker does not translate or remap ownership automatically.

The most common cause is a UID/GID mismatch. On the host, the directory might be owned by your user (UID 1000). Inside the container, the application might run as a different user — often root (UID 0), node (UID 1000 in some images, but not all), www-data (UID 33), or nobody (UID 65534). If the container process’s UID does not match the file owner’s UID and the file permissions do not allow other users to write, you get permission denied.

Other causes include:

  • The volume is mounted as read-only. A trailing :ro on the mount flag makes the volume read-only inside the container. This is intentional but easy to forget.
  • SELinux is blocking access. On Fedora, RHEL, CentOS, and other SELinux-enabled distributions, Docker volumes need special labels (:z or :Z) for the container to access them. Without these labels, SELinux silently denies access even though standard Unix permissions would allow it.
  • Docker Desktop file sharing restrictions. On macOS and Windows, Docker Desktop requires explicit file sharing configuration for host directories. If the directory is not in the shared list, the mount may fail silently or produce permission errors.
  • The container runs as root but the host directory is owned by a non-root user with restrictive permissions. This is less common since root can usually read anything, but it happens when the filesystem is mounted with special options like root_squash on NFS.

Understanding which of these causes applies to your situation determines the correct fix.

Fix 1: Match the Container User to the Host UID/GID

The fastest fix for development is to run the container with the same UID and GID as your host user. This way, files created inside the container are owned by your host user, and your host user’s files are writable inside the container.

Find your host UID and GID:

id -u
id -g

On most single-user Linux systems, both are 1000.

Run the container with the --user flag:

docker run --user "$(id -u):$(id -g)" -v /home/user/data:/app/data myapp

This tells Docker to run the container process as your host user’s UID and GID. The process inside the container will have the same filesystem access as your user on the host.

In Docker Compose:

services:
  myapp:
    image: myapp
    user: "${UID}:${GID}"
    volumes:
      - ./data:/app/data

Set UID and GID in your .env file or export them before running docker compose up:

export UID=$(id -u)
export GID=$(id -g)
docker compose up

Caveats: Some containers expect to run as a specific user. Running as a different UID may break applications that check their username or need to read files owned by a specific user inside the image. If the application writes to directories inside the image (not on the mount), those directories must also be writable by the new UID. If your Compose stack has additional issues when starting, see Fix: Docker Compose up errors for further troubleshooting.

Fix 2: Set Ownership with chown in the Dockerfile

If you control the Dockerfile, you can create a non-root user with a specific UID and set ownership of the application directories. This is the recommended approach for production images.

FROM node:20-slim

# Create a group and user with specific UID/GID
RUN groupadd --gid 1000 appuser && \
    useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser

# Create the data directory and set ownership
RUN mkdir -p /app/data && chown -R appuser:appuser /app

WORKDIR /app
COPY --chown=appuser:appuser package*.json ./
RUN npm ci --omit=dev
COPY --chown=appuser:appuser . .

# Switch to the non-root user
USER appuser

CMD ["node", "server.js"]

Key points:

  • --chown=appuser:appuser on the COPY instruction ensures copied files are owned by the application user, not root.
  • The USER instruction switches all subsequent commands (and the final CMD) to run as appuser.
  • The UID 1000 matches the default user on most Linux hosts. If your host user has a different UID, change it in the Dockerfile or use a build argument.

Using a build argument for flexible UID:

ARG USER_UID=1000
ARG USER_GID=1000

RUN groupadd --gid $USER_GID appuser && \
    useradd --uid $USER_UID --gid appuser --shell /bin/bash --create-home appuser

Build with:

docker build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -t myapp .

This produces an image that matches your host user’s UID and GID exactly.

Fix 3: Fix Host Directory Permissions

Sometimes the simplest fix is to change the permissions on the host directory so the container user can write to it.

Make the directory writable by everyone (development only):

chmod 777 /home/user/data

This is insecure and should never be used in production, but it immediately unblocks development. For a deeper guide on Linux file permissions and why chmod 777 is dangerous, see Fix: EACCES permission denied.

Set ownership to match the container user:

# If the container runs as UID 1000
chown -R 1000:1000 /home/user/data

# If the container runs as www-data (UID 33)
chown -R 33:33 /home/user/data

Find out what user the container runs as:

docker run --rm myapp id

Or inspect the image:

docker inspect myapp --format '{{.Config.User}}'

If the User field is empty, the container runs as root (UID 0). If it shows a username, look up the UID in the container:

docker run --rm myapp id appuser

Pro Tip: Run docker run --rm myapp id to instantly see what UID/GID the container process runs as. This single command tells you exactly which user to match on the host side, saving minutes of guesswork.

Fix 4: Use Named Volumes Instead of Bind Mounts

Named volumes behave differently from bind mounts when it comes to permissions. When you create a named volume and mount it into a container, Docker initializes the volume with the contents and permissions of the target directory inside the image. This means if the image’s /app/data directory is owned by appuser, the named volume will inherit that ownership.

Bind mount (uses host permissions, often causes issues):

docker run -v /home/user/data:/app/data myapp

Named volume (Docker manages permissions):

docker run -v myapp-data:/app/data myapp

In Docker Compose:

services:
  myapp:
    image: myapp
    volumes:
      - myapp-data:/app/data

volumes:
  myapp-data:

Named volumes are stored under /var/lib/docker/volumes/ and are managed entirely by Docker. The container’s user can write to them because Docker sets up the initial permissions based on the image. This avoids the UID mismatch problem entirely.

When to use bind mounts vs named volumes:

  • Use bind mounts when you need the host and container to share files in real time (development, configuration files, source code hot-reloading).
  • Use named volumes when the container owns the data and the host does not need direct access (databases, caches, application state).

If your containers are running into storage issues with named volumes, see Fix: Docker no space left on device for cleanup strategies.

Fix 5: Add SELinux Labels (:z and :Z)

On SELinux-enabled systems (Fedora, RHEL, CentOS, Rocky Linux, Alma Linux), Docker volumes are denied access by SELinux even when Unix permissions are correct. The container process runs in a confined SELinux context that does not have permission to access arbitrary host directories.

Add the :z flag for shared volumes:

docker run -v /home/user/data:/app/data:z myapp

The lowercase :z tells Docker to relabel the host directory so that multiple containers can share it. Docker applies the container_file_t SELinux label to the directory.

Add the :Z flag for private volumes:

docker run -v /home/user/data:/app/data:Z myapp

The uppercase :Z relabels the directory for exclusive access by this one container. Only this container’s SELinux context can access the files. This is more secure but means no other container (and potentially no host process) can access the directory while the container is using it.

In Docker Compose:

volumes:
  - ./data:/app/data:z

How to tell if SELinux is causing the problem:

# Check if SELinux is enforcing
getenforce

# Temporarily disable SELinux to test
sudo setenforce 0
docker run -v /home/user/data:/app/data myapp
# If it works now, SELinux was the problem
sudo setenforce 1

Warning: Never leave SELinux permanently disabled. Use the :z or :Z labels instead. Running setenforce 0 is only for diagnosis.

If you are also seeing 403 errors from Nginx behind Docker, SELinux volume labels might be part of that problem too. See Fix: Nginx 403 Forbidden for more.

Fix 6: Remove the Read-Only Flag

If you accidentally mounted a volume as read-only, the container cannot write to it regardless of permissions.

Read-only mount (note the :ro suffix):

docker run -v /home/user/data:/app/data:ro myapp

Check if your volume is mounted read-only:

docker inspect mycontainer --format '{{json .Mounts}}' | python3 -m json.tool

Look for "RW": false or "Mode": "ro" in the output.

Fix by removing :ro:

docker run -v /home/user/data:/app/data myapp

In Docker Compose, remove the :ro from the volume definition:

volumes:
  - ./data:/app/data       # read-write (default)
  # - ./data:/app/data:ro  # read-only -- this was the problem

The :ro flag is useful for mounting configuration files or source code that the container should read but never modify. If you see the permission denied error and your mount includes :ro, that is almost certainly the cause.

Fix 7: Configure Docker Desktop File Sharing

On macOS and Windows, Docker runs inside a virtual machine. The host filesystem is not directly accessible to containers — Docker Desktop must explicitly share host directories with the VM.

Docker Desktop on macOS:

  1. Open Docker Desktop
  2. Go to Settings > Resources > File Sharing
  3. Add the directory you want to mount (e.g., /Users/yourname/projects)
  4. Click Apply & Restart

By default, Docker Desktop shares /Users, /Volumes, /private, /tmp, and /var/folders. If your directory is outside these paths, you must add it manually.

Docker Desktop on Windows:

  1. Open Docker Desktop
  2. Go to Settings > Resources > File Sharing
  3. Add the drive or directory (e.g., C:\Users\yourname\projects)
  4. Click Apply & Restart

WSL2 backend on Windows:

If you are using the WSL2 backend, file sharing works differently. Files under the WSL2 filesystem (/home/user/... inside the Linux distribution) are directly accessible. Files on the Windows filesystem (/mnt/c/...) go through a 9P filesystem bridge that is slower and can have permission issues.

For best performance and fewest permission issues, keep your project files inside the WSL2 filesystem rather than on the Windows drive.

Fix 8: Use Rootless Docker

Rootless Docker runs the Docker daemon and all containers under your regular user account without any root privileges. This changes how volume permissions work in a way that often resolves permission issues.

In rootless mode, the Docker daemon uses user namespaces. Your host UID is mapped to UID 0 (root) inside the container. This means when the container process runs as root and writes to a mounted volume, the files on the host are owned by your regular user — not by the system root.

Install rootless Docker:

dockerd-rootless-setuptool.sh install

Set the Docker host:

export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

Test it:

docker run --rm -v /home/user/data:/data alpine sh -c 'touch /data/test && ls -la /data/test'

The file /home/user/data/test will be owned by your host user, even though it was created by root inside the container.

Rootless Docker eliminates most permission issues for single-user development machines because the UID mapping aligns container root with the host user automatically. If your container has networking issues in rootless mode, see Fix: Docker container not connecting for guidance.

Fix 9: Use an Init Process for Signal Handling

This is not directly a permission fix, but it solves a related class of problems. When a container runs without an init process, the application runs as PID 1. PID 1 has special responsibilities in Linux — it must reap zombie child processes and handle signals correctly. If the application does not do this, child processes that write to the mounted volume may become zombies, hold file locks, or fail to flush writes.

Use Docker’s built-in init:

docker run --init -v /home/user/data:/app/data myapp

The --init flag injects a tiny init process (tini) as PID 1, which properly forwards signals and reaps child processes. Your application runs as PID 2.

In Docker Compose:

services:
  myapp:
    image: myapp
    init: true
    volumes:
      - ./data:/app/data

When this matters:

  • Applications that fork child processes to handle writes (e.g., background workers, log processors)
  • Containers that need to handle SIGTERM gracefully to flush buffered writes to the volume
  • Long-running containers where zombie processes accumulate and hold open file descriptors on volume files

Using --init is a best practice for all containers, not just those with volume issues. It costs almost nothing in terms of resources and prevents an entire category of subtle bugs.

Still Not Working?

Permission denied on the first write after container start

Some applications create directories or files during startup. If the mounted volume is empty, the application tries to create its directory structure. If it runs as a non-root user and the mount point inside the container is owned by root, the first write fails.

Fix this by creating the directory structure in the Dockerfile before the USER instruction:

RUN mkdir -p /app/data/logs /app/data/cache /app/data/uploads && \
    chown -R appuser:appuser /app/data

USER appuser

For bind mounts, create the directories on the host first:

mkdir -p ./data/logs ./data/cache ./data/uploads

Permission denied in a Docker-in-Docker setup

If you mount the Docker socket or directories into a container that runs Docker itself, the inner container inherits the outer container’s permission restrictions. The same UID/GID mismatch can occur at both levels.

Pass the --privileged flag if you need full access (development/CI only):

docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock docker:cli docker ps

For production, use rootless Docker-in-Docker or sysbox for proper isolation.

Permission denied only with specific files or subdirectories

If some files in the volume work fine but others do not, check for inconsistent ownership:

ls -la /home/user/data/
find /home/user/data -not -user $(id -u) -ls

Fix inconsistent ownership recursively:

chown -R $(id -u):$(id -g) /home/user/data/

This often happens when some files were created by the container (as the container user’s UID) and others were created on the host (as your UID). If the error manifests as a 403 from a web server reading from the volume, see Fix: Nginx 403 Forbidden.

NFS or network filesystem mounts

NFS volumes with root_squash enabled remap root access to nobody, which causes permission denied errors when the container runs as root. Either configure the NFS export with no_root_squash (less secure) or run the container as a non-root user whose UID has access to the NFS share.


Related: If Docker commands themselves fail with permission denied (not volume writes, but running docker at all), see Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles