A team ships a container image that runs its process as a non-root user and bind-mounts the source tree from each developer's machine. Describe the available strategies for making the container user's numeric ID line up with the host user's, and the tradeoffs of each.
answer
- --user $(id -u):$(id -g) → no passwd row, set HOME
- ARG UID + useradd -u → per-developer image
- entrypoint chown + exec gosu (not su/sudo)
- named volume over node_modules / target
- rootless Docker: container root == my UID
basics
~20 sOptions: pass --user with the host's numeric UID/GID at run time; bake the UID in at build time with a build argument; or start as root, chown the mount in an entrypoint, then drop privileges with gosu. Each trades reproducibility against portability.
solid answer
~60 sThere are three real strategies plus two escape hatches. 1. **Runtime `--user "$(id -u):$(id -g)"`** — one image works for every host UID. The cost is that the UID has no entry in the container's `/etc/passwd`: `whoami` fails, `HOME` may be unset, and any directory the image pre-created for that user is owned by the wrong UID, so writable paths must be group-writable or pre-chowned. 2. **Build-time `ARG UID`/`ARG GID` with `useradd -u`** — everything inside the image lines up correctly, names resolve, home directory works. The cost is a per-developer image build and drift when someone's UID differs (macOS 501, CI runners 1001). 3. **Root entrypoint that chowns the mount then `exec gosu app "$@"`** — robust and self-healing, at the cost of a recursive chown on start (slow on large trees) and needing to start with enough privilege to chown. Escape hatches: use **named volumes** for anything only the container touches, and adopt **rootless Docker**, where container root maps to your host UID and the whole class of bug disappears.
code
dockerfile · 8 linesFROM node:22-bookworm-slim
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID app \
&& useradd -u $UID -g $GID -m -s /bin/bash app
WORKDIR /src
USER app
CMD ["npm", "run", "dev"]go deeper
Know that --user with numeric IDs exists and that a chown in an entrypoint is the other common fix. You are not expected to weigh all the tradeoffs yet.
Compare at least runtime --user versus build-time ARG UID, and name a concrete downside of each (missing passwd entry; per-developer images). Mention named volumes for dependency directories.
Pick a default for the team and justify it, cover gosu-versus-su signal behaviour, recursive-chown cost, and the arbitrary-UID / GID-0 hardening pattern.
Argue about where writable state belongs at all, and whether rootless Docker or userns-remap removes the class of problem for the whole organisation rather than patching each image.
## The problem being solved When a host directory is bind-mounted into a container, the kernel enforces ordinary file permissions using the container process's numeric UID and GID against the host inode's numeric owner. If the two numbers do not agree, one of two failures follows: the container cannot write (permission denied on a developer-owned tree), or the container writes files the developer cannot subsequently modify. "Alignment" means making the number the container runs as either equal to, or group-compatible with, the number that owns the host files. ## Strategy 1 — runtime --user ``` docker run --user "$(id -u):$(id -g)" -v "$PWD:/src" myimage ``` This is the most portable option because a single published image serves every host. The kernel accepts any integer, existing user account or not. What breaks: the container's `/etc/passwd` has no matching row, so `getpwuid()` fails. Symptoms include `whoami: cannot find name`, an unset or `/`-valued `HOME` (npm, pip, gradle, and ssh then try to write caches into `/`), and prompt or tooling errors. Mitigations are setting `HOME` and cache directories explicitly via environment variables, or adding a passwd row at startup. Additionally, any path the Dockerfile created for the image's intended user (`/app/.cache`, `/home/appuser`) is now owned by the wrong UID, so those must be made group-writable or moved to a writable location. A common pattern is to give the image user group 0 and `chmod g+rwX` writable paths, since arbitrary UIDs typically still get GID 0 in some runtimes. ## Strategy 2 — build-time UID injection ``` ARG UID=1000 ARG GID=1000 RUN groupadd -g $GID app && useradd -u $UID -g $GID -m app USER app ``` Inside the image everything is coherent: the user has a name, a home directory, and owns its own caches. Nothing needs an entrypoint hack. The cost is that the image is now per-developer. Every host with a different UID needs its own build, so you cannot ship one artifact from CI to everyone, and the image you test is not byte-identical to the image a colleague runs. It also silently degrades: someone on macOS with UID 501 builds with the default 1000 and rediscovers the original bug. It works best when the whole team is standardised (all Linux, all UID 1000) or when the compose file passes `args: UID: ${UID:-1000}` and everyone exports it. ## Strategy 3 — chown in the entrypoint, then drop privileges ``` #!/bin/sh chown -R app:app /data exec gosu app "$@" ``` The container starts as root, fixes ownership of whatever it was handed, and then permanently drops to the unprivileged user with `gosu` or `su-exec` — both of which `exec` into the target process so PID 1 stays the app and signals still work (`su` and `sudo` fork, leaving an extra process that swallows SIGTERM). Costs and caveats: a recursive chown of a large tree (a `node_modules` with 300k inodes) can add many seconds to every start; you can narrow it to the specific directories that need writing. The container must begin with the ability to chown, which conflicts with policies that forbid starting as root. And on read-only or externally-managed mounts, chown may simply fail. A refinement is `fixuid`, which rewrites the container user's UID to match the mount's owner instead of rewriting the files. ## Escape hatch A — do not bind-mount writable state Much of the pain comes from putting *container-owned* output on a *human-owned* filesystem. Source code needs to be a bind mount because humans edit it. Build caches, dependency directories, and databases do not. Mounting a named volume over `/app/node_modules` or `/app/target` keeps ownership entirely inside Docker's control, removes the mismatch for those paths, and usually improves I/O performance on Docker Desktop as a bonus. ## Escape hatch B — rootless Docker and userns-remap Under rootless Docker, the daemon runs as the developer and container UID 0 is mapped to the developer's host UID via `/etc/subuid`. A container running as root therefore produces developer-owned files, which is exactly what people naively expect. Non-zero container UIDs map into the subordinate range and appear on the host as large unmapped numbers, so the technique is not free — but for the very common "container runs as root, writes to my tree" case it is the cleanest fix available. Rootful daemons can approximate this with `userns-remap` in `daemon.json`. ## Choosing For an image published to many machines, prefer `--user` plus group-writable paths, or an entrypoint chown. For a tightly controlled internal dev environment, build-time UID is simplest to reason about. In all cases, keep container-written state off bind mounts wherever a human does not need to read it directly.
- Why gosu or su-exec rather than su or sudo in an entrypoint?`gosu` and `su-exec` call exec, so the target process replaces the shell and becomes PID 1 directly. `su` and `sudo` fork a child and stay resident as the parent, which means PID 1 is now a wrapper that does not forward SIGTERM properly, so `docker stop` degrades into a 10-second timeout and SIGKILL. They also set up a login environment and TTY handling you usually do not want.
- How do you keep an image usable when the platform forces an arbitrary, unpredictable UID at run time?Make every writable path owned by group 0 and group-writable (`chgrp -R 0 /app && chmod -R g+rwX /app`), because arbitrary-UID runtimes typically still assign GID 0. Set `HOME` to a writable path explicitly so tools do not try to write into `/`. Avoid depending on a passwd entry for the running user, or generate one at startup.
saying these in an interview costs you the question
- chmod -R 777 on the host tree presented as the normal solution
- Using su or sudo in the entrypoint, breaking signal delivery to PID 1
- Assuming a matching username means matching UID
- Baking a hardcoded UID 1000 and calling it portable
- Chowning a huge dependency tree on every container start without noticing the startup cost