USER reference

Last reviewed on 2026-05-02

How USER changes the identity of subsequent build steps and the runtime process, why it matters for security, and how to avoid the file-ownership traps.

The USER instruction sets the user (and optionally the group) for all subsequent RUN, CMD, and ENTRYPOINT instructions in the Dockerfile, as well as the default user inside the container when it starts. Switching away from root before the container runs is the single most effective hardening step you can apply to a Docker image.

Syntax

USER <user>[:<group>]
USER <UID>[:<GID>]

You can refer to the user by name or by numeric ID. Numeric IDs are preferred in production: an orchestrator can verify them without resolving /etc/passwd inside the image, and Kubernetes' runAsNonRoot: true security context can only check numeric UIDs.

Why it matters

By default, processes inside a container run as root (UID 0). Container isolation is reasonably strong, but a process running as root inside a container can still:

Switching to a non-root user removes most of those concerns at near-zero cost.

Worked example: create and use a dedicated user

FROM debian:12-slim

# Install the application as root
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*

# Create a system user with a fixed UID/GID
RUN groupadd --system --gid 10001 app \
 && useradd --system --uid 10001 --gid app --create-home --home-dir /app app

WORKDIR /app
COPY --chown=app:app ./bin/server /app/server

# Drop privileges before running
USER 10001:10001

EXPOSE 8080
ENTRYPOINT ["/app/server"]

Several details matter here. The user is created with --system, which assigns it a UID outside the normal interactive range. The UID and GID are explicit (10001) so an external policy can pin to them. COPY --chown hands files over to the new user during the copy step rather than running a separate chown command, which would create an extra writable layer. The final USER uses the numeric form so Kubernetes runAsNonRoot can verify it without consulting /etc/passwd.

Distroless and scratch images

Distroless images do not have a shell or useradd, so you cannot create a user inside them. The standard pattern is to create the user in a builder stage, copy /etc/passwd and /etc/group entries (or the relevant lines) into the final stage, and then USER by numeric ID:

FROM debian:12-slim AS builder
RUN groupadd --system --gid 10001 app \
 && useradd --system --uid 10001 --gid app app
# ... build the binary ...

FROM gcr.io/distroless/static-debian12
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder --chown=10001:10001 /out/server /server
USER 10001:10001
ENTRYPOINT ["/server"]

Many official distroless images already ship a nonroot user (UID 65532); using the :nonroot tag of those images lets you skip the user-creation dance entirely.

Creating the user on Alpine and BusyBox images

Alpine ships BusyBox's adduser and addgroup, not the shadow-utils useradd used above. The flags differ, and a Dockerfile copied from a Debian example will fail on Alpine with useradd: not found:

FROM alpine:3.20

RUN addgroup --system --gid 10001 app \
 && adduser --system --uid 10001 --ingroup app --home /app --disabled-password app

WORKDIR /app
COPY --chown=10001:10001 ./bin/server /app/server

USER 10001:10001
ENTRYPOINT ["/app/server"]

The key differences: --ingroup replaces --gid on adduser, --disabled-password replaces --no-create-home-style flags, and there is no --create-home--home creates the directory on its own. If you need one Dockerfile to work on both families, create the user in a builder stage and copy the /etc/passwd and /etc/group lines forward, as in the distroless example above.

Choosing a UID

Pick a fixed, high, numeric UID and use it consistently. 10001 is a common convention; distroless uses 65532. Three rules keep it out of trouble:

Running a container as a different user

The USER in the Dockerfile is a default. Both the runtime and an interactive session can override it, which is how you connect to a container as a user other than root — or as root when the image defaults to something else:

# Start a container as a specific UID:GID, ignoring the image's USER
docker run --rm --user 10001:10001 myapp

# Match the host user, so files written to a bind mount stay yours
docker run --rm -v "$PWD:/work" --user "$(id -u):$(id -g)" myapp

# Open a shell in a running container as a non-root user
docker exec -it --user 10001 my-container sh

# Open a shell as root for debugging, even though the image drops privileges
docker exec -it --user 0 my-container sh

The --user "$(id -u):$(id -g)" form is the standard fix for the "files created in my bind mount are owned by root" problem on Linux. Note that the UID does not need to exist in the container's /etc/passwd; the process simply runs with an unmapped numeric ID, which is harmless for most workloads but will make whoami fail and leave $HOME unset.

Scope and ordering

File ownership pitfalls

The most frequent failure mode is files written as root in earlier RUN instructions that the non-root user cannot read or write. Two reliable patterns avoid this:

  1. Use COPY --chown for application files. A single COPY --chown=app:app ./build /app hands ownership over at copy time without an extra layer.
  2. Set WORKDIR after USER if the directory may not exist yet. A WORKDIR creates missing directories owned by the current user, so when it runs after USER it produces a writable home.

If the application needs to write to a specific path at runtime (a cache, an upload directory), declare it with VOLUME or document it for the orchestrator and ensure it is owned by the non-root UID.

Common mistakes

Pre-flight checklist

  1. Has a non-root user been created?
  2. Is the final USER a numeric UID, not a name?
  3. Does docker run --rm <image> id report a UID that is not 0?
  4. Can the application write to every path it expects to write to?
  5. Are exposed ports above 1024?