Tutorial

Docker volumes: fix permissions in 3 commands

Deployment10 min read10 steps

The app starts, the container runs, but logs show `Permission denied` and nothing gets saved. This issue affects virtually every self-hosted application the moment you mount a host directory into Docker. The cause is always the same: the UID of the user running inside the container doesn't match the owner of the directory on the host. This guide explains the mechanism, gives diagnostic commands, and provides the right fix for each scenario — without ever running your containers as root.

Contents· The mechanism: why Docker refuses to write1/9
  1. 01The mechanism: why Docker refuses to write
  2. 02Most affected applications and their UIDs
  3. 03Diagnosis: identify the problem in under 2 minutes
  4. 04Fix by case: chown on the host directory
  5. 05Variants: bind-mounts, named volumes and user:
  6. 06NFS and remote mounts
  7. 07Fix methods: comparison
  8. 08Troubleshooting: 4 classic errors with their exact messages
  9. 09Conclusion

The mechanism: why Docker refuses to write

Docker shares the host Linux kernel. When a container mounts a host directory (bind-mount), the filesystem applies the same POSIX permission rules. A file owned by UID 1000 on the host remains owned by UID 1000, regardless of whether it's called alice on the host or paperless in the container. If the process in the container runs as UID 472 and the directory is owned by root (UID 0), writes are denied — even with chmod 755.

Classic trap: you create the directory with your SSH session (UID 1000), then start a Grafana container running as UID 472. Grafana tries to write its SQLite database into /var/lib/grafana mounted from /opt/grafana/data — which belongs to your SSH user. Result: GF_PATHS_DATA='/var/lib/grafana' is not writable.

Most affected applications and their UIDs

Docker permissions issues mainly affect applications that run with a specific UID different from the host's.

  • Paperless-ngx — UID 1000 (user paperless): manages consume, export, media and data directories
  • Grafana — UID 472 (user grafana): writes SQLite database and plugins to /var/lib/grafana
  • Nextcloud (Debian image) — UID 33 (user www-data): data directory, config and logs
  • Immich — UID 1000 (user node): photo library, thumbnails and ML database
  • Gitea — UID 1000 (user git): repositories, SSH keys, logs and default SQLite database

Diagnosis: identify the problem in under 2 minutes

Before fixing, confirm it's a permissions problem and identify the UID involved. Three commands are enough.

  1. Read the exact error message

    Check the logs of the affected container:

    docker logs <container-name> 2>&1 | grep -i 'permission\|denied\|cannot\|mkdir'

    A permission denied or cannot create directory message confirms the diagnosis.

  2. Identify the process UID inside the container

    docker exec <container-name> id

    Typical output: uid=472(grafana) gid=0(root). Note the UID — here 472.

  3. Check the host directory owner

    ls -ln /opt/grafana/data

    Output drwxr-xr-x 2 0 0 ... means the directory is owned by root (UID 0). UID 472 only has "other" rights — read and execute, no write.

  4. Use stat for a complete diagnosis

    stat /opt/grafana/data

    Check the Uid: and Gid: lines. If they show (0/root) while your container runs as 472, the problem is confirmed.

  5. Inspect the container configuration

    docker inspect <container-name> | grep -A5 'Mounts'

    This command lists all active bind-mounts and named volumes, with their host source and container destination.

Fix by case: chown on the host directory

The basic fix is a chown of the host directory to the UID expected by the container. Here are the commands for the most common applications.

  1. Grafana (UID 472)

    mkdir -p /opt/grafana/data
    chown -R 472:472 /opt/grafana/data

    In your compose.yml:

    volumes:
      - /opt/grafana/data:/var/lib/grafana
  2. Nextcloud (UID 33, Debian image)

    mkdir -p /opt/nextcloud/{data,config,apps}
    chown -R 33:33 /opt/nextcloud/data
    chown -R 33:33 /opt/nextcloud/config

    Note: the Alpine image uses UID 82. Verify with docker exec <container> id www-data if you're unsure which variant you're using.

  3. Paperless-ngx (UID 1000)

    mkdir -p /opt/paperless/{consume,export,media,data}
    chown -R 1000:1000 /opt/paperless/consume
    chown -R 1000:1000 /opt/paperless/export
    chown -R 1000:1000 /opt/paperless/media
    chown -R 1000:1000 /opt/paperless/data
  4. Immich (UID 1000)

    mkdir -p /opt/immich/{library,thumbnails,encoded-video,profile}
    chown -R 1000:1000 /opt/immich

    Note: PUID/PGID environment variables do NOT work with official Immich images. They are specific to LinuxServer.io images.

  5. Verify after correction

    ls -ln /opt/grafana/data

    Should show drwxr-xr-x 2 472 472 .... Then restart the container:

    docker compose restart grafana
    docker logs grafana --tail 20

Variants: bind-mounts, named volumes and user:

The chown on the host directory works for bind-mounts, but Docker offers other approaches depending on the context.

user: directive in compose.yml. Some images are designed to accept an arbitrary UID via user:. This avoids chown if your directory belongs to your host user:

services:
  app:
    image: my-image
    user: "1000:1000"
    volumes:
      - /home/user/data:/app/data

This approach only works if the image doesn't require internal files owned by a specific UID (SUID binaries, sockets, etc.). Paperless-ngx supports this mode; Grafana does not.

Named Docker volumes. With a named Docker volume (docker volume create), Docker manages the directory under /var/lib/docker/volumes/. On first write, the directory is created with the container process permissions. Result: no permission issues at startup, but migrating existing data requires an explicit copy step via docker run --rm -v source:/from -v dest:/to alpine cp -a /from/. /to/.

NFS and remote mounts

NFS mounts add a layer of complexity: the NFS server applies its own UID rules. If the server exports with root_squash (default), root access from the client is reduced to nobody. If your container runs as UID 472 and the NFS share has no UID mapping on the server side, you get access denied even after a successful chown on the client.

Solutions:
1. Configure the NFS export with all_squash,anonuid=472,anongid=472 for Grafana, or the matching UID for your application.
2. Use no_root_squash only if you fully control the network (security risk).
3. For CIFS/SMB, pass uid=33,gid=33 in mount options for Nextcloud — CIFS doesn't allow ownership changes after mount.

For CIFS mounts in /etc/fstab:

//server/share /opt/nextcloud/data cifs uid=33,gid=33,credentials=/etc/cifs-creds,iocharset=utf8 0 0

Fix methods: comparison

Scroll the table

MethodAdvantagesRisks / Limitations
chown UID:GID on host directorySimple, universal, compatible with all imagesMust know the exact UID; must redo if directory is recreated
user: UID:GID in compose.ymlNo chown needed; portable between hostsImage must support arbitrary UIDs; may break internal files
Named Docker volumesPermissions handled automatically on first startupLess transparent; migrating existing data is more complex
--privileged or chmod 777Immediately fixes the problemDANGER: exposes host and all processes; never use in production

Troubleshooting: 4 classic errors with their exact messages

Here are the four most common Docker errors related to permissions, with the exact error message and its solution.

  • mkdir: cannot create directory '/var/lib/grafana/plugins': Permission denied → Host directory doesn't belong to UID 472. Apply chown -R 472:472 /opt/grafana/data.
  • [Errno 13] Permission denied: '/usr/src/paperless/media' → Paperless-ngx can't write to its media directory. Verify the bind-mount belongs to UID 1000 on the host.
  • Could not create lock file /var/lib/grafana/.~lock.grafana.db → Grafana can read the directory but not write to it. Permissions issue on existing files: chown 472:472 /opt/grafana/data/*.db or delete the lock file.
  • chown: changing ownership of '/data': Operation not permitted (at container startup) → The image tries to fix permissions itself but can't because it doesn't run as root. Perform chown manually on the host BEFORE starting the container.

SELinux and AppArmor: :z and :Z flags in bind-mounts. On systems with active SELinux (CentOS, RHEL, Fedora), a bind-mount can be blocked even if POSIX permissions are correct. Docker provides two suffixes: :z relabels the content for sharing between multiple containers, and :Z relabels for private access (single container). Example: - /opt/grafana/data:/var/lib/grafana:z. Without this flag on a SELinux system, you'll get Permission denied even after a correct chown. To check if SELinux is blocking: ausearch -m AVC -ts recent | grep docker.

Conclusion

The Permission denied in Docker logs is never inevitable. The three-command solution: docker exec <container> id to find the UID, ls -ln <host-directory> to confirm the current owner, then chown -R <UID>:<GID> <host-directory> to fix it. The golden rule: create host directories with the correct owner before starting the container, not after. On a dedicated VPS, this discipline prevents 90% of data silently lost on restart incidents.

A VPS ready for self-hosting

Deploy your Docker applications on a ServOrbit VPS with root access, dedicated IPv4 and automatic snapshots. Starting from 99 DH/month.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab