Matching Container UID and GID to the Host User
On a Linux laptop, after running the stack, git status fails with error: insufficient permission for adding an object to repository database .git/objects, rm -rf dist asks for sudo, and the editor refuses to save generated files: EACCES: permission denied, open 'src/generated/api.ts'. ls -l shows the culprits owned by root or by an unknown user 1001. The container wrote them through a bind mount as a different user than the one on the host. This page makes containers write files as the host user, as part of volume mounting and hot-reload optimization.
This is mainly a Linux problem. Docker Desktop on macOS and Windows translates ownership on bind mounts, which is why the same Compose file works for colleagues on Macs and breaks for Linux users — the topic's macOS and Windows permission guide covers the other direction.
Diagnostic
Compare the host user's IDs with the user each container runs as, and find files the container created:
#!/usr/bin/env bash
set -euo pipefail
echo "host: uid=$(id -u) gid=$(id -g) user=$(id -un)"
for s in $(docker compose config --services); do
printf '%-8s %s\n' "$s" "$(docker compose exec -T "$s" id 2>/dev/null || echo 'not running')"
done
find . -path ./node_modules -prune -o \( ! -user "$(id -u)" -print \) 2>/dev/null | head -8
Expected bad output:
host: uid=1000 gid=1000 user=dev
web uid=0(root) gid=0(root) groups=0(root)
api uid=1001(app) gid=1001(app) groups=1001(app)
./web/dist
./web/dist/index.html
./api/src/generated/api.ts
./.git/objects/4f
The web container runs as root and the API as UID 1001; both write into bind-mounted directories, so the host sees files owned by 0 and 1001 that user 1000 cannot modify. The .git/objects/4f entry means a container even ran a git command in the mounted repository.
Root cause
Linux file ownership is a pair of numbers, and bind mounts pass files straight through to the host filesystem. A process running as UID 0 inside a container creates files owned by UID 0 on the host; a process running as UID 1001 creates files owned by 1001. User names inside the container are irrelevant — only the numbers are stored. Many images run as root by default, and images that add a non-root user usually pick an ID such as 1000 or 1001 that happens to match some developers and not others. Docker Desktop hides this on macOS and Windows by mapping ownership in its file-sharing layer, which is why the problem appears only for Linux developers, and why a fix tested on a Mac does not prove anything. The reliable fix is to run development containers that write to bind mounts with the host user's UID and GID.
Resolution
- Run the service as the host user by passing IDs through the environment:
services:
web:
build: ./web
user: "${UID:-1000}:${GID:-1000}"
environment:
HOME: /tmp
volumes:
- ./web:/app
- web-node-modules:/app/node_modules
volumes:
web-node-modules:
Bash does not export UID and GID to child processes by default, so set them for Compose:
#!/usr/bin/env bash
set -euo pipefail
grep -q '^UID=' .env 2>/dev/null || printf 'UID=%s\nGID=%s\n' "$(id -u)" "$(id -g)" >> .env
docker compose up -d --force-recreate web
HOME=/tmp gives tools that write to $HOME (npm, pip caches) a writable location, since the host UID may have no home directory inside the image.
- For images that need a named user, build one with the host IDs so tools that look up the user by name work too:
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm-slim
ARG UID=1000
ARG GID=1000
RUN groupmod -o -g "$GID" node && usermod -o -u "$UID" -g "$GID" node
USER node
WORKDIR /app
services:
web:
build:
context: ./web
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
The official Node image already has a node user; usermod -o changes its IDs to match the host instead of adding a second user.
- Keep named volumes owned by the container user. A named volume created by root is not writable by a non-root user; create the mount point in the image with the right owner so Docker copies that ownership into a fresh volume:
RUN mkdir -p /app/node_modules && chown node:node /app/node_modules
- Fix files already created with the wrong owner, once:
#!/usr/bin/env bash
set -euo pipefail
sudo chown -R "$(id -u):$(id -g)" web/dist api/src/generated .git
git fsck --no-progress | head -3 || true
Expected output
$ docker compose exec -T web id
uid=1000(node) gid=1000(node) groups=1000(node)
$ docker compose exec -T web sh -c 'cd /app && npm run build >/dev/null' && ls -ln web/dist | head -3
total 12
-rw-r--r-- 1 1000 1000 1873 Sep 18 11:02 index.html
drwxr-xr-x 2 1000 1000 4096 Sep 18 11:02 assets
$ git status --short | head -2
M web/src/cart/Total.tsx
The container runs as UID 1000, files it creates belong to the host user, and git and the editor work without sudo.
The same Compose file keeps working for colleagues on macOS: UID and GID come from their .env too, Docker Desktop's file sharing maps ownership as before, and the numeric user inside the container is simply whatever their Mac user ID is. That portability matters because the alternative — Linux-only override files — tends to rot, since most of the team never runs them.
Prevention
Set
UIDandGIDin the bootstrap target so every Linux developer's.envhas them before the firstdocker compose up.Check for foreign-owned files in
make doctorwith thefind ! -usercommand from the diagnostic, which catches regressions the day they happen.Never run git inside containers against a bind-mounted repository unless the container runs as the host user; it is the fastest way to end up with root-owned objects in
.git.
Platform caveats
macOS and Windows (Docker Desktop): ownership is translated by file sharing, so the problem rarely appears; setting
user:is harmless and keeps behaviour consistent with Linux colleagues.
Rootless Docker and Podman: UID 0 inside the container maps to the host user, so root-in-container files are owned by you on the host. Other container UIDs map to subordinate IDs; use
userns_mode: keep-idon Podman, as described in running Compose projects with Podman.
SELinux (Fedora, RHEL): ownership may be correct and access still denied; add
:zto bind mounts to relabel content.
Rollback
Remove the user: lines and build arguments; containers return to their image defaults:
#!/usr/bin/env bash
set -euo pipefail
git checkout HEAD~1 -- compose.yaml web/Dockerfile
docker compose up -d --build --force-recreate
Frequently Asked Questions
Why are files created by my container owned by root?
The container process runs as UID 0 and writes through a bind mount; Linux stores the numeric owner unchanged. Run the service with user: "${UID}:${GID}" so files are created as your host user.
Why does this only happen on Linux?
Docker Desktop on macOS and Windows maps file ownership in its file-sharing layer, so container UIDs are not visible on the host. On Linux, bind mounts are direct, so ownership is exactly what the container wrote.
Why does npm install fail after setting user:?
npm writes its cache to $HOME, which may not exist or be writable for an arbitrary UID. Set HOME=/tmp or a volume-backed home directory for that service.
Is running containers as root in development a security problem?
It is mostly an ownership problem locally, but root in a container is also root on the host for anything bind-mounted. Running as the host user avoids both.