Meta description: I chased a Docker permission denied error across three environments before finding the real fix — here’s what actually works on Linux shared volumes.
Last updated: July 5, 2026
I still remember staring at EACCES: permission denied, open '/app/uploads/file.png' inside a container log, convinced my code was fine because it ran perfectly on my Mac. The problem wasn’t my code — it was a classic Docker permission denied error caused by UID/GID mismatches between my host Linux machine and the container’s user. It took me an embarrassingly long afternoon to understand why, and an even longer one to fix it properly instead of just running everything as root.
If you’re bind-mounting a host directory into a container on Linux and getting permission errors, this article walks through exactly why it happens and the three fixes I actually use in production.
TL;DR
- Docker on Linux shares the host’s user namespace by default, so a container process running as UID 1000 needs the host directory to actually allow UID 1000 — permissions aren’t magically translated.
- The real fix is matching UID/GID between host and container (via build args or
usermod), notchmod 777or running as root. - For multi-service shared volumes, Docker’s user namespace remapping (
userns-remap) is the more robust long-term solution, though it has real trade-offs I’ll cover.
Background: Why This Happens on Linux (But Not Always on Mac/Windows)
On Docker Desktop for Mac and Windows, the Docker VM handles a translation layer between the host filesystem and the Linux VM running your containers, which often masks permission mismatches. On native Linux, there’s no such VM — the container shares the host kernel directly, and Linux file permissions are enforced by UID/GID numbers, not usernames. If your container process runs as UID 1000 but the bind-mounted host folder is owned by UID 1001 (or root, UID 0), the write simply fails, exactly as it would between two Linux users on the same machine.
This is why the exact same docker-compose.yml behaves differently on a teammate’s Mac versus your Ubuntu CI runner — the underlying permission model is fundamentally different.
[INTERNAL LINK: related article]
Prerequisites
- A Linux host (I tested this on Ubuntu 24.04 and Debian 12)
- Docker Engine 26.x (not Docker Desktop)
- A Dockerfile using a non-root
USERdirective, or a base image likenode:20-alpinethat already drops privileges docker composev2 syntax
Step-by-Step Implementation
Step 1: Confirm it’s actually a UID mismatch
Before assuming anything, check both sides:
ls -ln ./uploads
This shows the numeric UID/GID owning the host folder — for example 1000 1000. Then check what UID the container runs as:
docker run --rm my_image id
If you see uid=1001(node) while the host folder is owned by 1000, that mismatch is your entire problem.
Step 2: Fix it by building the container with a matching UID
The cleanest fix I’ve found is passing your host UID as a build argument, so the container user is created to match:
FROM node:20-alpine
ARG UID=1000
ARG GID=1000
RUN addgroup -g ${GID} appgroup && \
adduser -D -u ${UID} -G appgroup appuser
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
Build it passing your actual host UID:
docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t my_image .
Pro Tip: I add this to a Makefile target so every teammate builds with their own UID automatically, instead of hardcoding
1000and breaking for anyone whose Linux UID differs (common on shared CI runners or LDAP-managed dev machines).
Step 3: Fix ownership on the host side (quick fix, not ideal long-term)
If rebuilding isn’t an option right now, you can align ownership from the host:
sudo chown -R 1000:1000 ./uploads
This works, but it’s brittle — it only fixes the current state and will drift again the moment a new file gets created by a different UID.
Security Note: Never reach for
chmod -R 777as a fix. It resolves the symptom by removing all permission enforcement, meaning any process or container on the host can read and write those files. I’ve seen this cause real data-integrity issues in shared staging environments.
Step 4: Use Docker’s user namespace remapping for a durable fix
For a setup with multiple services touching the same shared volume, I configure userns-remap in /etc/docker/daemon.json:
{
"userns-remap": "default"
}
Restart the daemon to apply it:
sudo systemctl restart docker
This remaps container UIDs to an unprivileged range on the host, so even UID 0 inside a container maps to a non-root, non-privileged UID outside it. It’s the most robust fix for shared multi-tenant Docker hosts, though it does have a real trade-off: some images and volume plugins that expect literal UID matches (like certain Postgres or Elasticsearch images) can break under remapping and need explicit exceptions.
Step 5: Verify the fix
docker run --rm -v $(pwd)/uploads:/app/uploads my_image touch /app/uploads/test.txt
ls -l ./uploads/test.txt
If this succeeds without sudo and without chmod 777, the fix is holding.
Real-World Tips I Use in Production
- I standardize on UID 1000 across all base images in a project, since it’s the default first-user UID on most Linux distros — this alone prevents 90% of mismatches for new team members.
- For CI pipelines, I explicitly pass
--build-arg UID=$(id -u)in the pipeline script rather than assuming a fixed UID, since CI runners sometimes use different UIDs than local dev machines. - I avoid bind-mounting
node_modulesorvendordirectories across host/container boundaries entirely when I can, using named volumes instead — sidesteps permission mismatches completely for dependency folders.
Common Errors and How I Fixed Them
Error: EACCES: permission denied, mkdir '/app/uploads' Root cause was the container’s appuser (UID 1001) trying to write into a host folder owned by UID 1000. Fix: rebuilt with matching --build-arg UID.
Error: Works locally, fails in CI with the same Dockerfile The CI runner’s checkout user had a different UID than my local machine. Fix: passed $(id -u) dynamically in the CI script instead of hardcoding UID=1000.
Error: Postgres container fails to start after enabling userns-remap Postgres’s data directory ownership checks didn’t match the remapped UID range. Fix: added postgres to the userns-remap exceptions list in daemon.json rather than disabling remapping globally.
FAQ
Q: Why do I get permission denied errors only on Linux and not Mac? A: Docker Desktop on Mac runs containers inside a VM with a translation layer, which often hides UID/GID mismatches that are enforced directly on native Linux hosts.
Q: Is running the container as root a valid fix for Docker permission errors? A: It resolves the error but removes an important security boundary — I only use it for quick local debugging, never in shared or production environments.
Q: What does Docker userns-remap actually do? A: It maps container UIDs, including root, to an unprivileged range on the host, so a container process can’t gain real root privileges on the host filesystem.
Q: How do I find the UID a Docker container is running as? A: Run docker run --rm your_image id and compare the output against ls -ln on the host directory you’re mounting.
Q: Why did chmod 777 stop the errors but feel wrong? A: It works by disabling permission enforcement entirely rather than fixing the mismatch, which opens the directory to unrestricted read/write from any user or process on the host.
Conclusion
Permission errors in Docker on Linux almost always come down to a UID/GID mismatch, not a broken container. Match your UIDs at build time, reach for userns-remap if you’re managing shared infrastructure, and resist the temptation to chmod 777 your way out of it. Run into a permission error I didn’t cover here? Tell me about it in the comments.
[SOURCE: https://docs.docker.com/engine/security/userns-remap/] [SOURCE: https://docs.docker.com/reference/dockerfile/#user]
About the Author
I’m a DevOps-focused software engineer with 8+ years running containerized infrastructure on Linux, currently working daily with Docker, Kubernetes, and CI/CD pipelines across mixed-OS development teams. My stack includes Docker Engine, Ubuntu Server, GitHub Actions, and Terraform. I write these breakdowns based on real incidents I’ve debugged, not theoretical setups.
