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(userpaperless): managesconsume,export,mediaanddatadirectories - Grafana — UID
472(usergrafana): writes SQLite database and plugins to/var/lib/grafana - Nextcloud (Debian image) — UID
33(userwww-data): data directory, config and logs - Immich — UID
1000(usernode): photo library, thumbnails and ML database - Gitea — UID
1000(usergit): 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.
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 deniedorcannot create directorymessage confirms the diagnosis.Identify the process UID inside the container
docker exec <container-name> idTypical output:
uid=472(grafana) gid=0(root). Note the UID — here472.Check the host directory owner
ls -ln /opt/grafana/dataOutput
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.Use stat for a complete diagnosis
stat /opt/grafana/dataCheck the
Uid:andGid:lines. If they show(0/root)while your container runs as 472, the problem is confirmed.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.
Grafana (UID 472)
mkdir -p /opt/grafana/data chown -R 472:472 /opt/grafana/dataIn your
compose.yml:volumes: - /opt/grafana/data:/var/lib/grafanaNextcloud (UID 33, Debian image)
mkdir -p /opt/nextcloud/{data,config,apps} chown -R 33:33 /opt/nextcloud/data chown -R 33:33 /opt/nextcloud/configNote: the Alpine image uses UID 82. Verify with
docker exec <container> id www-dataif you're unsure which variant you're using.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/dataImmich (UID 1000)
mkdir -p /opt/immich/{library,thumbnails,encoded-video,profile} chown -R 1000:1000 /opt/immichNote:
PUID/PGIDenvironment variables do NOT work with official Immich images. They are specific to LinuxServer.io images.Verify after correction
ls -ln /opt/grafana/dataShould 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/dataThis 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 0Fix methods: comparison
Scroll the table
| Method | Advantages | Risks / Limitations |
|---|---|---|
| chown UID:GID on host directory | Simple, universal, compatible with all images | Must know the exact UID; must redo if directory is recreated |
| user: UID:GID in compose.yml | No chown needed; portable between hosts | Image must support arbitrary UIDs; may break internal files |
| Named Docker volumes | Permissions handled automatically on first startup | Less transparent; migrating existing data is more complex |
| --privileged or chmod 777 | Immediately fixes the problem | DANGER: 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. Applychown -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/*.dbor 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. Performchownmanually 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.