You did the responsible thing: you mounted a volume so your data survives container restarts.
Then your application boots and faceplants on Permission denied like it just discovered Linux for the first time.
This isn’t a “Docker is buggy” moment. It’s Linux doing exactly what you asked—just not what you meant.
Containers don’t have “users.” They have numbers. And your volume is owned by numbers too.
When those numbers don’t line up, you get the classic production-grade error message: “no.”
The real problem: users are numbers, volumes are filesystems
Docker didn’t invent permissions. It inherited them. A container process runs with a UID and one or more GIDs,
just like any other Linux process. A volume (bind mount or named volume) is backed by a real filesystem with real
inode ownership (UID/GID) and mode bits (rwx). Docker merely connects the two.
When an image says “run as appuser,” what it really means at runtime is “run as UID 1001 (or 999, or 70, or whatever the image builder picked).”
Meanwhile, your host directory might be owned by UID 1000. Or root. Or an LDAP-backed UID that doesn’t exist inside the container.
Linux doesn’t care about names. It compares integers. If they don’t match and there’s no group/ACL permission, access fails.
Named volume vs bind mount: the permission trap changes shape
Two common mounting styles, two slightly different failure modes:
-
Bind mount (
-v /host/path:/container/path): the container sees the host directory exactly, including ownership and ACLs.
Great for debugging; brutal for “works on my machine” drift. -
Named volume (
-v myvol:/container/path): Docker creates and manages a directory under its data root.
Ownership is still UID/GID-based, but now it’s created by Docker/daemon defaults and sometimes “helped” by image entrypoints.
The uncomfortable truth: chmod 777 is not a fix
If you’ve ever “solved” this with chmod -R 777, you didn’t fix permissions. You declared bankruptcy.
It usually works, and it usually comes back later as a security incident, a compliance finding, or a mystery data corruption story.
The right approach is boring: make the container’s runtime UID/GID match the volume’s ownership (or vice versa), intentionally, repeatably.
One quote worth keeping on a sticky note:
“Hope is not a strategy.”
— Gene Kranz
Permissions debugging is where hope goes to die.
Fast diagnosis playbook
When you’re on-call, you don’t need a lecture. You need a fast path to the root cause.
Here’s the order that tends to end the pain quickly.
First: confirm what UID/GID the process is actually running as
- Check container runtime user (
idinside the container, or inspectUserin config). - If it’s root and still failing, suspect SELinux/AppArmor, NFS root-squash, or read-only mounts.
Second: check the mount type and its on-disk owner/mode
- Is it a bind mount or named volume?
- On the host, inspect ownership (
stat) and any ACLs (getfacl).
Third: check the filesystem and security layers
- SELinux enforcing? Missing
:Z/:zlabeling on bind mounts? - NFS/CIFS with root-squash or mapped identities?
- Rootless Docker or user namespaces shifting IDs?
Then: pick one fix strategy and make it deterministic
- Best default: run the container with the host UID/GID and pre-create directories.
- Alternative: chown the volume once (carefully) via an init step, not on every boot.
- Avoid: perpetual recursive chown in entrypoints for large datasets.
Joke #1: Recursive chown is the closest thing Linux has to a meditation app. It forces you to sit and think about your choices.
The volume fix that actually ends it
The fix that survives rebuilds, node changes, and human creativity is simple:
align UID/GID between the container process and the volume ownership.
Do it explicitly, not by “whatever the image author did.”
The most reliable pattern for Docker Compose
When you control the host directory and you’re using bind mounts, set user in Compose to the host user’s UID/GID,
and make the host directory owned by that UID/GID.
cr0x@server:~$ id
uid=1000(cr0x) gid=1000(cr0x) groups=1000(cr0x),27(sudo),998(docker)
Decision: if your service should write to a bind-mounted directory owned by your deploy user (UID 1000),
run the container as 1000:1000. You’re not “making it less secure.” You’re making it predictable.
Example Compose snippet (conceptual; implement in your stack):
- Host:
/srv/myapp/dataowned by1000:1000, mode0750or tighter. - Container: process runs as
1000:1000.
If you must use a named volume
Named volumes are fine. They’re also opaque enough that teams start guessing.
The sane approach is:
- Create the volume.
- Initialize ownership once, in a controlled, auditable step.
- Run the real app as a non-root UID that matches that ownership.
The anti-pattern is letting the main container run as root just to “fix permissions” on startup.
That’s how you end up with a service that is root forever because someone is afraid to touch it.
What about “just chown it”?
Chowning can be correct. It can also be catastrophic.
On large volumes, recursive chown is a full filesystem walk; on network storage, it’s a slow motion denial-of-service.
The trick is to do it once, and only if you’re sure you’re targeting the right path.
Patterns that work (and why)
Pattern A: Run the container as the host UID/GID (best default for bind mounts)
This is the “make Linux happy” approach. The filesystem already has ownership. Match the process to it.
It avoids chown storms and works well when your deployment process already has a stable service account.
It does require that the application can run as non-root. Most modern images can.
If an image insists on root for no good reason, consider it a smell.
Pattern B: Initialize volume ownership once (best default for named volumes)
You create a small one-shot container whose only job is to create directories and set ownership/ACLs,
then you run the app as a normal unprivileged user.
Done right, it’s deterministic. Done wrong, it’s a footgun with extra steps. The difference is:
you target exact paths, avoid recursive chown unless necessary, and log what you did.
Pattern C: ACLs instead of ownership (great when multiple UIDs need write)
Sometimes you have sidecars or multiple containers writing to the same mount (backup agent, log shipper, app).
Ownership can’t satisfy everyone unless you force everyone to share a UID (which gets messy).
ACLs let you grant write access to additional UIDs/GIDs without making the directory world-writable.
Pattern D: Use group ownership + setgid directories (classic Unix, still good)
If multiple processes need to create files in the same directory, make it group-writable and set the setgid bit on the directory.
New files inherit the directory’s group. It’s one of those old-school Unix features that quietly solves real problems.
Pattern E: Rootless Docker and user namespaces (secure, but changes the math)
Rootless Docker and userns-remap are excellent security choices. They also rewrite UID/GID mapping.
Your container UID 0 might map to host UID 100000+.
That means a host directory owned by UID 0 is not writable by “root” in the container anymore—because it’s not really host-root.
This isn’t a bug. It’s the point.
Joke #2: “Just run it as root” is the container equivalent of “just reboot prod.” It works, and it also tells on you.
Practical tasks: commands, outputs, decisions
Below are real tasks you can run during incident response or preventative hardening.
Each one includes: the command, what the output means, and what decision to make.
Task 1: Identify the container’s effective UID/GID
cr0x@server:~$ docker exec -it myapp sh -lc 'id && umask'
uid=1001(app) gid=1001(app) groups=1001(app)
0022
Meaning: the process runs as UID/GID 1001 and creates files with default umask 0022.
Decision: the mounted directory must be writable by UID 1001 (owner or ACL) or by a group the process belongs to.
Task 2: Inspect which user Docker thinks it is running
cr0x@server:~$ docker inspect -f '{{.Config.User}}' myapp
1001:1001
Meaning: the container is configured to run as numeric user/group.
Decision: match the host directory ownership to 1001:1001, or change the runtime user to match the directory.
Task 3: Confirm the mount source and type
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' myapp
bind /srv/myapp/data -> /var/lib/myapp
volume myapp-cache -> /cache
Meaning: /var/lib/myapp is a bind mount; /cache is a named volume.
Decision: troubleshoot bind mount permissions on the host path; troubleshoot named volume via Docker’s volume location.
Task 4: Check host directory ownership and mode
cr0x@server:~$ stat -c 'path=%n owner=%u:%g mode=%a type=%F' /srv/myapp/data
path=/srv/myapp/data owner=0:0 mode=755 type=directory
Meaning: owned by root, not writable by others (755 means only owner can write).
Decision: either chown to 1001:1001, or run container as root (not recommended), or use ACL/group write.
Task 5: Simulate access using a disposable container as a specific UID
cr0x@server:~$ docker run --rm -u 1001:1001 -v /srv/myapp/data:/mnt alpine sh -lc 'touch /mnt/test && ls -ln /mnt/test'
touch: /mnt/test: Permission denied
Meaning: UID 1001 cannot write to the bind mount; the failure is reproducible outside your app.
Decision: fix the filesystem permissions first; don’t waste time “debugging the app.”
Task 6: Fix bind mount ownership (targeted, not recursive unless needed)
cr0x@server:~$ sudo chown 1001:1001 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=755
Meaning: directory owner is now UID/GID 1001. Mode 755 means owner can write.
Decision: re-test write access. If the app needs group collaboration, adjust mode/ACL accordingly.
Task 7: Use setgid + group write for shared directories
cr0x@server:~$ sudo chgrp 1001 /srv/myapp/data
cr0x@server:~$ sudo chmod 2775 /srv/myapp/data
cr0x@server:~$ stat -c 'owner=%u:%g mode=%a' /srv/myapp/data
owner=1001:1001 mode=2775
Meaning: setgid bit is set (2xxx). New files inherit group 1001; group has write.
Decision: use this when multiple processes share a GID and you want predictable group ownership.
Task 8: Add an ACL for a second writer UID without loosening mode bits
cr0x@server:~$ sudo setfacl -m u:1002:rwx /srv/myapp/data
cr0x@server:~$ getfacl -p /srv/myapp/data | sed -n '1,12p'
# file: /srv/myapp/data
# owner: 1001
# group: 1001
user::rwx
user:1002:rwx
group::rwx
mask::rwx
other::r-x
Meaning: UID 1002 explicitly has rwx on the directory; mask allows it.
Decision: pick ACLs when you have multiple UIDs across containers and you don’t want “everyone is 1000” cosplay.
Task 9: Locate a named volume on the host (for debugging and initialization)
cr0x@server:~$ docker volume inspect myapp-cache -f '{{.Mountpoint}}'
/var/lib/docker/volumes/myapp-cache/_data
Meaning: the named volume lives under Docker’s data root.
Decision: use this path for host-level inspection (stat, getfacl) or for cautious one-time permission setup.
Task 10: Inspect named volume ownership and confirm container UID mismatch
cr0x@server:~$ sudo stat -c 'owner=%u:%g mode=%a' /var/lib/docker/volumes/myapp-cache/_data
owner=0:0 mode=755
Meaning: root-owned volume directory; container user 1001 can’t write.
Decision: initialize ownership once (targeted chown of just what you need), then run app unprivileged.
Task 11: Perform a one-time init to chown a volume safely (bounded scope)
cr0x@server:~$ docker run --rm -v myapp-cache:/cache alpine sh -lc 'addgroup -g 1001 app && adduser -D -u 1001 -G app app; mkdir -p /cache/app; chown -R 1001:1001 /cache/app; ls -ldn /cache/app'
drwxr-xr-x 2 1001 1001 4096 Feb 4 12:00 /cache/app
Meaning: created a subdirectory /cache/app owned by 1001:1001, without recursively chowning the entire volume root.
Decision: mount and use that subdirectory from the app to avoid unexpected ownership fights.
Task 12: Validate inside the app container that write works now
cr0x@server:~$ docker exec -it myapp sh -lc 'touch /cache/app/ok && ls -ln /cache/app/ok'
-rw-r--r-- 1 1001 1001 0 Feb 4 12:01 /cache/app/ok
Meaning: file created with correct numeric ownership.
Decision: you’re done—unless SELinux/AppArmor or NFS semantics are involved.
Task 13: Check for read-only mounts (surprisingly common)
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{if .RW}}{{else}}{{println "RO:" .Destination}}{{end}}{{end}}' myapp
RO: /var/lib/myapp
Meaning: the mount is read-only at the Docker level.
Decision: fix the Compose/run flags first; permissions won’t matter if the mount is RO.
Task 14: SELinux check (bind mounts that should work, but don’t)
cr0x@server:~$ getenforce
Enforcing
Meaning: SELinux is enforcing policy.
Decision: if bind mounts fail despite correct UID/GID/mode, label the bind mount appropriately (e.g., with :Z/:z) and re-test.
Task 15: Detect NFS root-squash behavior (root can’t fix what root can’t own)
cr0x@server:~$ mount | grep ' /srv/myapp '
nfs01:/exports/myapp on /srv/myapp type nfs4 (rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.10,local_lock=none,addr=10.0.0.20)
Meaning: the backing storage is NFSv4. Root-squash is configured server-side, not visible here.
Decision: if chown fails with “Operation not permitted,” stop fighting the container and fix identity mapping/export options.
Task 16: Check user namespace remapping (UIDs won’t match what you think)
cr0x@server:~$ docker info | sed -n '1,120p' | grep -E 'rootless|userns'
rootless: false
userns: host
Meaning: no userns remap; container UIDs map directly to host UIDs.
Decision: UID/GID alignment is straightforward. If userns is enabled, incorporate the remap ranges into your plan.
Task 17: Confirm the kernel sees the mount where you think it is
cr0x@server:~$ docker exec -it myapp sh -lc 'grep -E " /var/lib/myapp | /cache " /proc/mounts'
/dev/sda1 /var/lib/myapp ext4 rw,relatime 0 0
/dev/sda1 /cache ext4 rw,relatime 0 0
Meaning: mounts are present and rw at the kernel level.
Decision: if writes still fail, it’s permissions, SELinux/AppArmor, immutable attributes, or network filesystem semantics.
Task 18: Check immutable attribute (rare, but it happens)
cr0x@server:~$ sudo lsattr -d /srv/myapp/data
-------------------P-- /srv/myapp/data
Meaning: no immutable (i) attribute set.
Decision: if you see ----i--------, remove it with chattr -i (carefully) before chasing other ghosts.
Common mistakes: symptoms → root cause → fix
1) Symptom: “Permission denied” on startup, but only on one host
Root cause: bind mount points to a directory with different ownership/ACLs on that host (often created manually or by a different automation run).
Fix: standardize directory creation in provisioning. Enforce ownership/mode via config management. Run containers with explicit numeric UID/GID.
2) Symptom: container runs as root, still can’t write
Root cause: SELinux label mismatch, read-only mount, or NFS root-squash.
Fix: check mount RW flag; check getenforce; if NFS, fix export identity mapping; if SELinux, label the mount appropriately.
3) Symptom: “Operation not permitted” when chowning a mounted directory
Root cause: filesystem doesn’t support chown the way you expect (common with NFS/CIFS), or you’re not actually privileged to change ownership there.
Fix: align identities at the storage layer (UID mapping), or use ACLs supported by that filesystem, or write into a directory already owned correctly.
4) Symptom: works after deleting the volume, then breaks again later
Root cause: you “fixed” it by resetting state. The next run recreates directories with root ownership or a different UID.
Fix: add a deterministic initialization step (create subdir + chown once) and stop relying on implicit defaults.
5) Symptom: files created by the app are owned by root on the host
Root cause: container process is running as root, or the image entrypoint switches users incorrectly, or you used user: "0" as a quick fix.
Fix: run as a non-root numeric UID; ensure the entrypoint doesn’t re-escalate; verify with id inside the container.
6) Symptom: log shipper/backup sidecar can’t read files created by the app
Root cause: umask too strict, no shared group strategy, or no ACL for the second UID.
Fix: set group ownership + setgid on directories; or add an ACL; or align both containers to a shared GID.
7) Symptom: after enabling rootless Docker, everything is “Permission denied”
Root cause: host paths are owned by real root, but rootless container “root” maps to an unprivileged host UID range.
Fix: chown the host directories to the rootless user (or adjust subuid/subgid mappings), or avoid bind mounts that require host-root ownership.
8) Symptom: massive startup delay when the container starts
Root cause: entrypoint does recursive chown on a large mounted dataset.
Fix: remove recursive chown from the hot path; do a one-time init; target a subdirectory; or use filesystem-level provisioning.
9) Symptom: permissions look correct, but writes fail only in production
Root cause: production uses SELinux enforcing or a different storage backend (NFS/CephFS) with different semantics.
Fix: test with production-like security settings; explicitly handle SELinux labels and network filesystem identity mapping.
Three corporate mini-stories from the trenches
Mini-story #1: The incident caused by a wrong assumption
A team rolled out a containerized job runner that wrote artifacts to a bind-mounted directory under /srv/builds.
In staging, it was fine. In production, artifacts started failing with Permission denied at random.
The on-call rotated through the usual suspects: storage outage, disk full, broken image.
The wrong assumption was subtle: “the host directory is always created by our automation, so it’s always owned by the service account.”
Except one production host had been rebuilt in a hurry during a maintenance window. A human recreated /srv/builds manually.
Root owned it, mode 755, and the container ran as UID 1001.
The failure pattern looked random because jobs were scheduled across multiple hosts. The ones landing on the “handmade” host failed.
Nobody noticed the ownership difference until someone ran stat across the fleet and saw the odd one out.
The fix wasn’t heroic. They added a simple preflight check in provisioning that asserted ownership and mode, and they set Compose to run the container as 1001:1001 explicitly.
Most importantly, they stopped relying on usernames in documentation—every runbook started using numeric UID/GID.
The lesson: if a directory’s existence is part of correctness, then its ownership is also part of correctness. Linux doesn’t do vibes.
Mini-story #2: The optimization that backfired
Another org had a big data processing service. To reduce operational toil, they added a startup step that did
chown -R app:app /data on every container boot. It made support tickets disappear—briefly.
It also made deployments “reliable,” in the way that waiting two hours is reliably slow.
At first, it was only a nuisance. Then the dataset grew. A rolling update meant multiple containers in parallel, each recursively walking a multi-terabyte tree.
CPU spiked, metadata I/O spiked, and the storage backend started throwing latency alerts. The app was “healthy,” the cluster was not.
The backfire was not just performance. The recursive chown touched timestamps and ownership on files that were used by downstream systems to detect “freshness.”
A bunch of pipelines reprocessed data because ownership changed, which changed metadata, which triggered their “new data” heuristics.
Nobody was amused.
The fix was to move permission initialization out of the runtime container entirely:
a one-time provisioning job created a dedicated subdirectory and set ownership once.
They also changed the application to refuse to start if it couldn’t write, instead of trying to “repair” permissions. Fail fast, fail honest.
The lesson: “self-healing permissions” is often just “slowly burning your filesystem.”
Mini-story #3: The boring but correct practice that saved the day
A platform team standardized a policy: every stateful container must declare a numeric user in Compose/Kubernetes,
and every writable mount must be provisioned with matching ownership before the workload is scheduled.
No exceptions, no “temporary root,” no 777. It annoyed developers for about a week.
Months later, they migrated a fleet of services from local disks to NFS-backed storage to simplify backups.
NFS introduced its usual identity-mapping quirks. Predictably, a few services started throwing permission errors during the first canary.
But because the UID/GID was declared and consistent, the troubleshooting surface was tiny: it was either export mapping or directory provisioning.
The on-call didn’t guess. They checked the declared UID, checked the directory ownership on the NFS server, fixed mapping, and moved on.
No image rebuilds. No emergency “run as root.” No midnight chmod.
The practice was boring: explicit numeric identities, pre-provisioned writable paths, and a preflight check in CI that validated the container runs non-root.
That boredom paid rent.
The lesson: standardization beats cleverness, especially around permissions.
Interesting facts & historical context
- UIDs and GIDs predate containers by decades. Docker didn’t create the model; it rides on classic Unix ownership and mode bits.
- Names are decoration. Linux stores ownership as integers in inodes; usernames are resolved later via
/etc/passwdor NSS. - Early container setups defaulted to root. It was convenient, and it trained a generation to normalize running services as UID 0.
- User namespaces changed the definition of “root.” With userns or rootless mode, container root can map to an unprivileged host UID range.
- Overlay filesystems didn’t “fix” permissions. OverlayFS solves layering; mounted volumes still obey the backing filesystem’s ownership and policy.
- NFS root-squash is older than most container platforms. It’s a storage-side security feature that makes root inside a client behave like “nobody.”
- ACLs are not new. POSIX ACLs have been around for years; they’re just underused because mode bits are simpler to explain.
- SELinux labeling is a separate axis from UID/GID. You can have perfect ownership and still get denied by MAC policy.
- Many official images standardized on fixed UIDs. Database images often use stable numeric IDs so volumes remain compatible across upgrades—when you respect those IDs.
Checklists / step-by-step plan
Step-by-step plan: make a stateful container stop fighting its volume
-
Pick the runtime identity.
Decide the numeric UID/GID the service should run as. Use a dedicated, stable number (not “whatever the base image picked today”). -
Pick a mount strategy.
Bind mount for explicit host control; named volume for portability; network storage only if you understand its identity model. -
Provision the writable path.
Create the directory (or a subdirectory) and set ownership/mode. Prefer targeted chown over recursive. -
Make it testable.
Add a preflight check: can the container write a temp file to the mount at startup? If not, fail fast with a clear log line. -
Keep init separate from runtime.
If you need to chown, do it as a one-time init job or a manual operational step, not on every container start. -
Handle SELinux/AppArmor explicitly.
If you run SELinux enforcing, bake mount labeling into your run/compose configuration. -
Document numeric IDs.
Put UID/GID in the repo next to Compose manifests. Names drift; numbers don’t. -
Rehearse the disaster recovery path.
If you restore a volume from backup, does it preserve ownership? If not, what re-ownership step is required?
Pre-deploy checklist (use this before the first production run)
- Container runs as non-root (verify
idinside container). - Mount destinations are writable by that UID/GID (verify with a disposable
touchtest). - No recursive chown in entrypoint for large data paths.
- SELinux enforcing systems have correct labeling strategy for bind mounts.
- Network filesystems have identity mapping plan (UID parity or directory owned appropriately).
- Backup/restore process preserves or re-applies ownership deterministically.
Incident checklist (when “Permission denied” wakes you up)
- Confirm container UID/GID (
docker exec ... id). - Confirm mount type and RW status (
docker inspect). - Check host path ownership/mode/ACL (
stat,getfacl). - Check SELinux enforcing (
getenforce) and audit logs if applicable. - Check for NFS/CIFS and root-squash symptoms (
mount, chown behavior). - Apply the smallest safe fix: ownership alignment, ACL, or correct labeling—not 777.
FAQ
1) Why does my container user exist in the image but not on the host?
Because they’re separate namespaces for user names. Only numeric IDs matter for filesystem permission checks.
The host doesn’t need to know the username; it needs the UID/GID to match ownership or granted permissions.
2) If I run the container with -u 1000:1000, do I need that user inside the image?
Not strictly. The kernel enforces numeric IDs. Some applications expect a passwd entry for the UID (for getpwuid() calls).
If the app complains, add an entry or use an image that supports arbitrary UIDs.
3) Is it safe to chown a Docker volume directory under /var/lib/docker?
It can be, but be disciplined. Prefer creating and owning a subdirectory inside the volume rather than changing the volume root.
Avoid recursive chown on large data. And never run “guessy” commands as root in that tree during an incident.
4) Why do permissions break after restoring from backup?
Many backup tools preserve content but not numeric ownership, or they restore as the user running the restore.
If the restored files end up owned by root (or a different UID), your container user loses write access.
Fix by restoring with ownership preserved or running a controlled re-ownership step afterward.
5) What’s the difference between chmod and chown in this context?
chown changes who owns the files (which UID/GID gets “owner rights”).
chmod changes what owners/groups/others can do. If the owner is wrong, chmod often just rearranges disappointment.
6) My app runs as root in the container. Isn’t that fine because it’s “isolated”?
Isolation is not absolute. Root in the container has wide powers within that container context, and misconfigurations happen.
Running as non-root reduces blast radius, and it forces you to solve volume permissions correctly instead of papering over them.
7) Why does it work on macOS/Windows Docker Desktop but fail on Linux?
Docker Desktop uses a VM and a different filesystem sharing mechanism. Ownership and permission semantics can be translated or simplified.
Linux hosts are “real” in the sense that the bind mount is the actual filesystem with actual UIDs, ACLs, and security modules.
8) How do I handle multiple containers writing to the same volume?
Prefer a shared GID strategy (group write + setgid) or use ACLs to grant write to multiple UIDs.
Avoid running everything as the same UID unless you’re prepared for the auditing and debugging consequences.
9) What if I’m using Kubernetes instead of Docker Compose?
The principles are the same: align runtime UID/GID with storage permissions. Kubernetes adds securityContext options like
runAsUser and fsGroup. Be careful: fsGroup can trigger recursive permission changes on some volume types,
which is great for small volumes and painful for huge ones.
10) Is umask part of this story?
Yes. Even if directory ownership is correct, a restrictive umask can create files that other processes can’t read.
When you have sidecars or shared readers, decide intentionally whether group read/write is required and set umask accordingly.
Conclusion: next steps that stick
“Permission denied” on Docker volumes is rarely mysterious. It’s nearly always mismatched numeric UID/GID, a missing group/ACL permission,
or a security layer (SELinux, NFS root-squash) doing its job.
The fix that lasts is to stop improvising and start declaring identity and ownership as part of your deployment contract.
Do this next (in order)
- Pick a stable numeric UID/GID for each stateful service and write it down in the repo.
- Provision writable directories intentionally (owner/mode, or ACLs, or group+setgid), not by accident.
- Run containers as that UID/GID and verify with a simple write test during startup or CI.
- Remove recursive chown from entrypoints unless your data path is tiny and you enjoy slow deploys.
- Account for SELinux and network storage early; they don’t become easier at 2 a.m.
If you treat volumes like part of the application—not an afterthought—you’ll stop playing permission whack-a-mole.
Your future self will still get paged, of course. Just for something more interesting.