What does the USER instruction in a Dockerfile do, and what else must change in the image when you add it before the final CMD?
answer
- Config.User, applies to later RUN + CMD
- numeric UID so platforms can verify non-root
- COPY --chown, else files stay root-owned
- <1024 ports need CAP_NET_BIND_SERVICE
- --user overrides it: default, not sandbox
basics
~20 sUSER sets the UID/GID that later build steps and the container's main process run as. Without it everything runs as root (UID 0). Recipe: create a user, chown the app files to it, then USER before CMD.
solid answer
~50 sUSER <uid>[:<gid>] switches identity for all subsequent RUN steps and for CMD/ENTRYPOINT at runtime; it is stored in the image config as Config.User. The default is root, and root in a container is the same UID 0 as on the host unless the daemon is rootless or uses user-namespace remapping, so a bind mount or a breakout carries real root power. Recipe: install packages while still root, create a fixed non-root account, COPY --chown= the app in, then USER 10001:10001 just before CMD. Prefer a numeric UID, because platforms that enforce run-as-non-root cannot resolve a username against the image's /etc/passwd. What breaks: any path the app writes at runtime must be chowned at build time or mounted with matching ownership, and a non-root process cannot bind ports below 1024, so listen on 8080. USER is only a default: docker run --user root overrides it.
code
dockerfile · 8 linesFROM eclipse-temurin:21-jre
RUN groupadd -g 10001 app && useradd -u 10001 -g 10001 -m app
WORKDIR /app
COPY --chown=10001:10001 build/libs/app.jar /app/app.jar
RUN mkdir -p /app/tmp && chown 10001:10001 /app/tmp
EXPOSE 8080
USER 10001:10001
CMD ["java", "-jar", "/app/app.jar"]go deeper
Know the syntax, that it applies to everything after it, and the standard create-user, chown, USER, CMD ordering.
Explain ownership of copied files and writable paths, numeric versus named UID, and the low-port restriction.
Frame it as one control among several (drop capabilities, no-new-privileges, read-only rootfs) and describe how you enforce and verify it across images.
Discuss org-wide UID conventions, base images that ship a nonroot user, admission-style enforcement of non-root, and the residual risk when the daemon itself is not rootless.
## What it does USER sets the Linux user, and optionally group, for the rest of the build and for the running container. Form: `USER <user|uid>[:<group|gid>]`. Every instruction after it executes as that identity, and the value is written into the image configuration field Config.User, which the runtime applies to the container's main process. ## Why the default is a problem With no USER the process runs as UID 0. Unless the daemon runs rootless or with user-namespace remapping, that UID 0 is the host's UID 0: a bind-mounted host directory is writable as root, and any breakout starts from root. Running as an unprivileged UID also blocks a whole class of in-container mischief (installing packages, writing to /etc, writing into system paths). ## Creating the user The image must contain the account before you switch to it, because RUN steps after USER are unprivileged. On Debian/Ubuntu bases: `groupadd -g 10001 app && useradd -u 10001 -g 10001 -m app`; on Alpine: `addgroup -g 10001 app && adduser -D -u 10001 -G app app`. Some minimal bases (distroless, several vendor images) ship a nonroot account already. ## Numeric UID versus name A name is resolved inside the container against /etc/passwd. Outside tooling that wants to verify the image does not run as root has no cheap way to do that resolution, so it fails closed or lets a name-mapped root slip through. `USER 10001:10001` is unambiguous everywhere and still works if /etc/passwd is missing entirely, which is the case in scratch-based images. If the app needs a resolvable name (some libraries call getpwuid), add the passwd entry explicitly. ## File ownership COPY and ADD write files owned by root by default. After USER the app can read them (world-readable) but cannot write. Use `COPY --chown=10001:10001 . /app`. Directories the app must write at runtime (caches, temp, upload dirs) need `mkdir` plus `chown` while still root, or better, mount a writable volume and keep the image filesystem read-only. ## Ports Binding a TCP port below 1024 requires CAP_NET_BIND_SERVICE or a relaxed net.ipv4.ip_unprivileged_port_start sysctl. The idiomatic fix is to listen on a high port (8080, 8443) and publish it to whatever host port you want; the host-side port number is unrelated to the in-container one. ## It is a default, not a boundary `docker run --user 0` overrides Config.User, and an image running as non-root can still be given extra capabilities or privileged mode. USER is one layer: combine it with dropped capabilities, no-new-privileges, and a read-only root filesystem. Verify what shipped with `docker inspect --format '{{.Config.User}}' img` and `docker run --rm img id`.
- Why is USER 10001 preferable to USER app?A username has to be resolved inside the container against /etc/passwd, so external checks cannot cheaply prove the image is non-root, and the instruction fails outright if the account is missing (scratch or distroless images). A numeric UID needs no resolution, works with an empty /etc/passwd, and lets a platform enforce a non-root policy statically.
- After adding USER, the app fails with permission denied writing to /var/lib/app. How do you fix it in the Dockerfile?While still root, create the directory and chown it to the runtime UID: RUN mkdir -p /var/lib/app && chown 10001:10001 /var/lib/app, placed before the USER line. If the path is a volume or bind mount at runtime, the image chown does not help - the mount's ownership on the host or the volume wins, so set ownership there or run with a matching UID.
- Does running as non-root make the container secure?No. It removes one privilege level, but the process can still reach the network, read anything world-readable, and abuse any capability or mount it was given. Pair it with dropping capabilities, --security-opt no-new-privileges, a read-only root filesystem, and a runtime policy that rejects --user 0.
saying these in an interview costs you the question
- Thinking root inside the container is a different, harmless root than host root
- Assuming COPY files are automatically owned by the USER
- Believing USER prevents an operator from running the container as root anyway
- Expecting a non-root container to bind port 80 with no extra capability
- Putting USER at the top of the Dockerfile so package installs fail