Docker File Permissions Between Host and Container: A Clean Fix
AI generated
Docker · File Permissions · UID/GID · Security · DevOps
Docker File Permissions Between Host and Container
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.

14 min read UID · GID · userns-remap · fixuid · Bind Mounts · Named Volumes Docker Engine 24+ · Linux · rootless Docker

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.

11. FAQ: Docker File Permissions Between Host and Container

1Why do container files end up owned by root on the host?
Host and container share the kernel UID namespace. UID 0 inside the container is UID 0 on the host. --user $(id -u) solves the problem directly.
2Named volume versus bind mount for file permissions?
Bind mounts mirror host permissions. Named volumes are managed by Docker, with no host side conflicts. Prefer named volumes for data internal to the container.
3What does fixuid do?
Dynamically adjusts the container user's UID to the supplied UID at startup. Ideal for teams with different host UIDs who share the same image.
4Setting up a non-root user in the Dockerfile correctly?
Create with adduser using a fixed UID 1000. Hand over directories with chown before the USER statement. Use COPY --chown. Place USER at the end of the build process.
5What is rootless Docker?
The Docker daemon runs without root, inside the user's own namespace. Bind mount files automatically belong to the correct host user. The cleanest long term fix.
6userns-remap versus rootless Docker?
userns-remap: the daemon runs as root, container UIDs get remapped to high host UIDs. Rootless: the daemon runs entirely without root. Both prevent privilege escalation, rootless offers stronger isolation.
7Root for installation, then a regular user?
Use a multi-stage build: install as root in the build stage. In the final stage, copy only the artifacts, run chown, set USER. The production process never runs as root.
8Which user is a container running as?
docker exec CONTAINER id shows UID/GID. docker run --rm IMAGE id shows the default user. Use trivy or docker scout for security scanning of root processes.
9--user fails even though the UID is correct?
The application looks up the UID in /etc/passwd or HOME. If the UID is not listed there, those lookups fail. Add fixuid or libnss-wrapper to fill in the entry dynamically.
10Securing file permissions in CI/CD?
Use trivy and hadolint in CI to scan for root processes. Set USER explicitly. Prefer named volumes for build caches. Produce artifacts as the correct user.