solved cleanly and for good
Files created inside a container as root that cannot be edited on the host, volume mounts that refuse read or write access, processes running with UID 0 even though there is no real need for it: Docker file permission issues are among the most common problems in daily development and production operations. UID/GID mapping, fixuid, and rootless Docker solve these conflicts systematically.
Table of Contents
- 1. How Docker file permissions and UID/GID mapping work
- 2. The classic permission problem with bind mounts
- 3. The --user flag: the simplest fix for many cases
- 4. Dockerfile: setting users and permissions right from the start
- 5. fixuid: dynamic UID/GID adjustment inside the container
- 6. Named volumes versus bind mounts: when to use which
- 7. userns-remap: system-wide UID isolation
- 8. Rootless Docker: the safest approach
- 9. Approaches compared side by side
- 10. Summary
- 11. FAQ
1. How Docker file permissions and UID/GID mapping work
Docker containers share the host's kernel but not its user namespace. In practice that means numeric UID 1000 inside a container is the very same UID 1000 on the host system: there is no automatic translation between the two. When a process inside the container runs as UID 0 (root) and writes a file to a mounted volume, that file ends up root owned on the host too. A local developer with UID 1000 can then no longer edit it, because the file permissions are set to 640 or 600 with root as the owner.
This behavior is not a bug, it is the Linux permission system working exactly as designed. Docker file permission problems always show up where the UID of the process inside the container does not match the UID of the host user who wants to open or edit the files. This happens most often with bind mounts, that is, whenever a directory from the host is mounted into the container. The fix lies in UID/GID mapping: container processes need to run with the same UID as the host user who owns the files, or the permissions must be set explicitly so that every party involved can write to them.
2. The classic permission problem with bind mounts
The typical scenario looks like this: a developer mounts their local project directory as a bind mount into a PHP or Node container. Inside the container, a build or install step runs and creates new files. Those new files belong to the UID of the process inside the container, often UID 0 (root) or another fixed UID set in the Dockerfile with USER. Afterward the local developer can no longer edit those files in their editor or commit them with git, because the Docker file permissions lock them out.
The problem gets worse when CI/CD pipelines use the same image. In the pipeline the container runs as UID 0, the build creates files as root, and the following stage, which runs as an unprivileged user, cannot read them. These Docker file permission failures in CI are hard to debug because they depend on the environment and often do not show up locally, where the container also happens to run as root. The root cause is always the same: the UID of the container process does not match the UID of the expected file owner.
# Diagnose: check which UID a container process runs as
docker run --rm nginx id
# uid=0(root) gid=0(root): runs as root, will create root-owned files
# Check file ownership after writing to a bind mount
docker run --rm -v "$(pwd):/workspace" node:20 \
sh -c "npm install"
ls -la node_modules/
# drwxr-xr-x root root node_modules/ <- owned by root, not by your user
# Quick fix: explicitly pass host UID/GID to the container process
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$(pwd):/workspace" \
-w /workspace \
node:20 npm install
ls -la node_modules/
# drwxr-xr-x 1001 1001 node_modules/ <- owned by your host user
3. The --user flag: the simplest fix for many cases
The --user flag on docker run is the most direct way to fix Docker file permission problems. With --user $(id -u):$(id -g) the container process runs with the UID and GID of the currently logged in host user. Files the process writes then belong to the host user and are directly accessible to them. In Docker Compose the same effect can be achieved with the user directive, either as a static value or as an environment variable.
The --user flag does have one important limitation: the user with the given UID must exist inside the container, or the application must not depend on user specific configuration such as a home directory or a password file. Many applications write to their home directory on startup or read from /etc/passwd. If the UID is not listed in /etc/passwd, those operations fail. That is why the flag alone is not always enough, and why approaches like fixuid become necessary.
4. Dockerfile: setting users and permissions right from the start
The cleanest foundation for correct Docker file permissions starts in the Dockerfile itself. A dedicated non-root user is created with RUN addgroup and adduser, given access to all application directories, and set as the default user for the container process. The USER statement in the Dockerfile ensures that every subsequent RUN, CMD, and ENTRYPOINT instruction runs as that user. In multi-stage builds, the non-root user is typically defined in the final stage, once every build step that needs root access has already finished.
A common trap around Docker file permissions in a Dockerfile: directories get created as root and the user is switched afterward, without transferring ownership of those directories first. The container process then cannot write to its own working directories. The correct pattern is to always run COPY --chown=user:group and RUN mkdir ... && chown user:group dir before the USER statement, while the build is still running as root.
# Dockerfile: proper user setup for correct file permissions
FROM php:8.4-fpm-alpine
# Create application user with fixed UID/GID matching typical host user
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
# Install dependencies as root before switching user
RUN apk add --no-cache git
WORKDIR /var/www/html
# Copy files and set ownership in a single layer (no extra chown layer)
COPY --chown=appuser:appgroup . .
# Create writable directories and hand them to appuser
RUN mkdir -p var/cache var/log pub/media && \
chown -R appuser:appgroup var/ pub/
# Switch to non-root user: all subsequent commands run as appuser
USER appuser
# Verify: image must never run as root in production
# docker run --rm myimage id -> uid=1000(appuser) gid=1000(appgroup)
5. fixuid: dynamic UID/GID adjustment inside the container
fixuid is a small Go program that rewrites the UID and GID of the container user to an externally supplied UID/GID when the container starts. The result: the container process runs with the host user's UID, without the image having to be rebuilt for every developer. fixuid is wired in as an entrypoint wrapper and reads the desired UID from the FIXUID environment variable or from the --user flag.
This approach is especially valuable for team development environments where different developers have different UIDs. With fixuid, every developer can use the very same Docker image and still get correct Docker file permissions on every mounted file. Configuration lives in a config.yml file that gets copied into the image and defines the default UID and GID, as well as the paths whose ownership should be adjusted on startup. In the Compose file, user: "${UID}:${GID}" is then set, with UID and GID coming from the local shell environment.
6. Named volumes versus bind mounts: when to use which
Named volumes and bind mounts behave fundamentally differently when it comes to Docker file permissions. A bind mount mounts a host directory into the container together with all of its existing permissions and ownership. A named volume, on the other hand, is a managed directory that Docker creates, which starts out empty and gets populated from the image layer the first time the container runs. Named volumes belong to Docker and are stored in a Docker managed area on the host.
For application data that is only needed inside the container, such as caches, generated assets, or database files, named volumes are the better choice, because they never create Docker file permission conflicts with the host user. For source code that a developer wants to edit locally and test directly in the container, bind mounts are necessary, and in that case explicit UID mapping or fixuid is required. An elegant hybrid strategy combines a bind mount for the source code with named volumes for writable directories such as var/cache or vendor, which the container manages exclusively.
7. userns-remap: system-wide UID isolation
User namespace remapping (userns-remap) is a Docker daemon setting that maps every container UID, system wide, onto a shifted range of the host's UID space. With "userns-remap": "default" in the Docker daemon configuration, for example, UID 0 inside a container gets mapped to UID 100000 on the host. That means even if a container process has root privileges, its files on the host live in the UID range 100000 to 165535 and therefore have no access whatsoever to host files.
userns-remap solves an important security problem tied to Docker file permissions: container processes that break out of their container end up on the host as unprivileged users with high UIDs. For development environments, however, userns-remap has a downside: bind mounts behave differently under userns-remap, and existing volumes need to be reinitialized. On CI/CD systems or shared hosts, where many teams share the same Docker engine, userns-remap is an important safeguard against privilege escalation via Docker volumes.
# /etc/docker/daemon.json: enable user namespace remapping
{
"userns-remap": "default"
}
# After daemon restart: verify remapping is active
docker info | grep "Security Options"
# Security Options:
# userns
# Check mapped UIDs for a running container
cat /etc/subuid # shows the allocated range, e.g.: dockremap:100000:65536
# Named volume ownership under userns-remap
# Docker automatically adjusts volume ownership when userns-remap is active
docker volume create myapp_data
docker run --rm -v myapp_data:/data alpine stat /data
# should show uid=0 inside container = uid=100000 on host
# docker-compose.yml: user mapping is handled automatically under userns-remap
# No --user flag needed: root in container is unprivileged on host
services:
app:
image: myapp:latest
volumes:
- myapp_data:/var/www/html/var
volumes:
myapp_data:
8. Rootless Docker: the safest approach
Rootless Docker is an operating mode in which the Docker daemon itself runs without root privileges. Every user can run their own Docker instance, fully isolated inside their own user namespace. Docker file permission problems with bind mounts largely disappear under rootless Docker, because the daemon runs with the user's UID and every container process operates inside that user's namespace. Files that a container process writes to a bind mount automatically belong to the correct user on the host.
Installing rootless Docker requires a few system prerequisites: newuidmap and newgidmap must be installed, and the user needs sub-UID and sub-GID entries in /etc/subuid and /etc/subgid. Compared to classic Docker, rootless Docker has some limitations: certain networking features, privileged ports below 1024, and some storage drivers are not available. For most development and production workloads, though, these limitations are irrelevant, and rootless Docker offers the cleanest fix for Docker file permission problems.
9. Approaches compared side by side
There are several ways to tackle Docker file permission problems, and they differ in effort, security, and suitability for different scenarios.
| Approach | Effort | Security | Recommendation |
|---|---|---|---|
| --user $(id -u) | Minimal | Good | Simple development setups |
| Dockerfile USER + chown | Low | Very good | Production: always |
| fixuid | Medium | Good | Team development, mixed UIDs |
| Named Volumes | Low | Very good | Internal container data |
| userns-remap | High | Maximum | Shared hosts, security critical |
| Rootless Docker | Medium | Maximum | New setups, CI/CD |
The recommendation for most teams: always define a non-root user with a fixed UID in the Dockerfile, and use named volumes for every writable directory the container manages internally. Only for source code bind mounts in development is the --user flag or fixuid additionally needed. Rootless Docker is the cleanest long-term solution and is actively promoted by both Docker Inc. and Red Hat (Podman).
Mironsoft
Docker security, file permission architecture, and secure CI/CD setups
Docker file permissions under control for good?
We analyze existing Docker setups for file permission problems, implement secure UID/GID strategies, and retrofit existing Dockerfiles and Compose configurations with correct user setups.
Dockerfile audit
Analysis for root processes, wrong permissions, and insecure volume configurations
UID/GID strategy
fixuid integration or rootless Docker for team development environments
Production hardening
Integrating userns-remap, read-only filesystems, and security scanning into the CI pipeline
10. Summary
Docker file permission problems arise whenever the UID of the container process does not match the UID of the expected file owner on the host. The most fundamental fix is to always define a non-root user in the Dockerfile with USER, and to hand over ownership of all application directories with chown before switching users. For development environments with bind mounts, the --user $(id -u):$(id -g) flag or fixuid solves the problem without having to adapt the image for every developer.
Named volumes are preferable for every writable directory the container manages internally, because they never create host side Docker file permission conflicts. Rootless Docker and userns-remap are the safest approaches for production and multi-tenant environments, because they ensure that container processes on the host never operate with genuinely privileged UIDs. The combination of correct Dockerfiles, smart volume strategies, and UID mapping covers every common permission problem.
Docker File Permissions: The Essentials at a Glance
Dockerfile basics
Always set USER with a fixed UID, hand over directories with chown before the USER switch. No root processes in production.
Development environment
--user $(id -u):$(id -g) or fixuid for bind mounts. Named volumes for vendor, cache, and generated files.
Named versus bind mounts
Named volumes for data internal to the container, bind mounts only for source code edited locally. A hybrid strategy combines both.
Production security
Rootless Docker or userns-remap on production servers. Prevents privilege escalation even if a container breakout occurs.