skip to content

USER, WORKDIR & Metadata Instructions

The supporting instructions that set who runs, where, and what the image declares about itself, from USER and WORKDIR through LABEL, HEALTHCHECK and STOPSIGNAL. Interviewers check them because running as root is an everyday review finding.

part ofDockeroverview, primer and where to startread it →
on this pageshow

questions

6

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?

level: juniorimportance: must knowfreq 76%

answer

  1. Config.User, applies to later RUN + CMD
  2. numeric UID so platforms can verify non-root
  3. COPY --chown, else files stay root-owned
  4. <1024 ports need CAP_NET_BIND_SERVICE
  5. --user overrides it: default, not sandbox

basics

~20 s

USER 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 s

USER <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 lines
dockerfile
FROM 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

for a junior

Know the syntax, that it applies to everything after it, and the standard create-user, chown, USER, CMD ordering.

for a middle

Explain ownership of copied files and writable paths, numeric versus named UID, and the low-port restriction.

for a senior

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.

for a principal

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

context

open as a page

What does WORKDIR do in a Dockerfile, and why does `RUN cd /app` not have the same effect?

level: juniorimportance: must knowfreq 64%

basics

~20 s

WORKDIR sets the working directory for every following instruction and for the container's main process, and creates it if missing. Each RUN runs in its own shell, so a cd inside one RUN is forgotten when that step ends.

open as a page

What does the EXPOSE instruction in a Dockerfile actually do at runtime, and how do you make a container's port reachable from the host?

level: middleimportance: must knowfreq 58%

basics

~20 s

EXPOSE only records metadata: which ports the image intends to listen on. It publishes nothing. To reach a port from the host you must publish it at run time with -p host:container, or -P to map every EXPOSEd port to a random host port.

open as a page

How does the Dockerfile HEALTHCHECK instruction work, and what do its --interval, --timeout, --retries and --start-period options control?

level: middleimportance: must knowfreq 52%

basics

~20 s

HEALTHCHECK CMD runs a command inside the container on a schedule: exit 0 means healthy, 1 means unhealthy. --interval is the gap between checks, --timeout kills a hung check, --retries is how many consecutive failures flip the status, --start-period is a startup grace window where failures do not count.

open as a page

How do you attach metadata such as source repository, version and build revision to a container image, and which key names are standardized?

level: middleimportance: should knowfreq 38%

basics

~20 s

Use LABEL key=value in the Dockerfile; values land in the image config and are readable with docker inspect. Use the standard OCI keys org.opencontainers.image.* (source, revision, version, created, title, licenses) so tooling and registries recognize them. MAINTAINER is obsolete.

open as a page

What does the STOPSIGNAL instruction in a Dockerfile control, and when would you set it to something other than SIGTERM?

level: seniorimportance: should knowfreq 30%

basics

~20 s

STOPSIGNAL sets which signal the runtime sends to the container's main process on a stop request; the default is SIGTERM, and SIGKILL follows after the grace period. Change it when the application's graceful shutdown uses a different signal, as nginx does with SIGQUIT.

open as a page