Fixing Docker User Permissions: A Non-Root Security Guide
Learn how to implement docker user permissions correctly. Stop running containers as root and fix those annoying "docker volume permission denied" errors.
Running containers as root is the default, but it's a security liability I try to avoid whenever possible. If your container gets compromised, the attacker starts with root access to the container's filesystem. While that’s not always full host access, it’s a massive head start for any exploit.
I’ve spent plenty of time fighting "permission denied" errors when trying to switch to a non-root user. You define a user, run the container, and suddenly your app can't write to the mounted volume because the host UID doesn't match the container UID.
Why the "Permission Denied" Error Happens
When you mount a volume from your host machine into a container, the kernel maps the file ownership based on numeric UIDs (User IDs), not usernames.
If your host user has a UID of 1000 and your Dockerfile creates a user with UID 1001, the containerized process won't have write access to files created by your host user. This is a classic mismatch that creates a "docker volume permission denied" scenario. It’s frustrating, but it’s actually a sign that your security boundaries are working.
Implementing Non-Root Security Best Practices
The most robust way to handle this is to align your container's UID with your host's UID during the build or at runtime. Here is the strategy I use for most of my projects.
- Create a specific user in your Dockerfile.
- Ensure the application directory is owned by that user.
- Use build arguments to allow flexibility.
Here is what a clean Dockerfile looks like:
DockerfileFROM node:20-slim # Create a non-root user ARG USER_ID=1000 ARG GROUP_ID=1000 RUN groupadd -g ${GROUP_ID} appgroup && \ useradd -u ${USER_ID} -g appgroup -m appuser WORKDIR /app RUN chown -R appuser:appgroup /app # Switch to the non-root user USER appuser COPY . . CMD ["node", "index.js"]
Dealing with Volume Mounts
Even with the setup above, if you mount a local folder into /app, the volume mount might override the ownership. If you're still seeing permission issues, you have two options:
- Option A: The
chownapproach. Run a quick command to fix permissions on the host side. This is easy but feels manual. - Option B: The Entrypoint script. Use a small shell script that runs as root during container startup, updates the UID of the existing user to match the mounted volume's owner, and then executes the application.
If you're interested in hardening your infrastructure further, check out my notes on Docker Security: Implementing Immutable Infrastructure via Read-Only Root. It’s a great next step after you’ve successfully migrated to non-root users.
Comparison: Managing Permissions
| Method | Security Level | Complexity | Best For |
|---|---|---|---|
| Root User | Low | Zero | Quick local testing |
| Fixed UID (1000) | Medium | Low | Standardized CI/CD |
| Dynamic Entrypoint | High | Medium | Shared dev environments |
A Quick Warning on Security
Dropping root is great, but it doesn't solve everything. You should also be looking at Preventing Arbitrary File Write Vulnerabilities in Node.js and PHP to ensure your application code isn't doing the heavy lifting for an attacker, even if your user permissions are locked down.
FAQ
Why shouldn't I just use chmod 777 on my volume?
Please don't. That grants read, write, and execute permissions to everyone on the system. It’s a massive security hole that makes your file system vulnerable to any process, regardless of user.
Does running as non-root affect network performance? Not at all. The kernel handles the network stack regardless of which user is running the process. The only restriction is that non-root users cannot bind to privileged ports (anything below 1024). You'll need to map your application to a higher port like 3000 or 8080.
Is it always necessary to match UIDs? If you aren't using volume mounts for persistent data, you don't need to match the host UID. However, for development workflows where you mount code or data, matching them saves you about 90% of the debugging headaches related to file access.
If you find yourself needing help architecting these setups for production, I offer CI/CD Pipeline & Docker Containerization services to help get your infrastructure to a "boring" and stable state.
I’m still experimenting with rootless Docker modes for more complex setups, but for 99% of web apps, the Dockerfile-based user management above is the sweet spot. It’s not perfect, but it’s a hell of a lot safer than running as root.
