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:
- Write anywhere in the writable filesystem layers.
- Bind privileged ports below 1024.
- Exploit kernel vulnerabilities with a wider blast radius if a sandbox escape exists.
- Trip security policies (PodSecurityAdmission "restricted", OpenShift defaults, EKS hardening profiles) that refuse to schedule root containers.
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.
Scope and ordering
USERapplies to every instruction after it. To run something as root and then drop, declare your build steps first and placeUSERjust before the runtime stage's exposed surface.- It can be set multiple times. The most recent
USERwins for the layer that follows. - A
USERset in a builder stage does not carry to a later stage. Each stage starts fresh. - The runtime
USERcan be overridden withdocker run --useror with a KubernetessecurityContext. Treat the Dockerfile setting as a sensible default, not a security boundary on its own.
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:
- Use
COPY --chownfor application files. A singleCOPY --chown=app:app ./build /apphands ownership over at copy time without an extra layer. - Set
WORKDIRafterUSERif the directory may not exist yet. AWORKDIRcreates missing directories owned by the current user, so when it runs afterUSERit 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
- Forgetting
USERentirely. An image with noUSERinstruction runs as root by default. Make this an explicit choice, not an oversight. - Switching to a non-root user, then trying to bind port 80 or 443. Non-root users cannot bind privileged ports without
cap_net_bind_service. Use a port above 1024 (commonly8080) and let your load balancer or ingress map externally. - Using a name that won't exist in production. If you set
USER appbut the runtime image lacks anappentry in/etc/passwd, the container fails to start. Numeric IDs are immune to this. - Mismatched volume ownership. When a host directory or named volume is mounted, its existing UID/GID is preserved. Either set them to match your container UID, run an init container to
chownthem, or use a volume policy that matches.
Pre-flight checklist
- Has a non-root user been created?
- Is the final
USERa numeric UID, not a name? - Does
docker run --rm <image> idreport a UID that is not 0? - Can the application write to every path it expects to write to?
- Are exposed ports above 1024?
Related
- FROM reference — minimal base images that already ship a
nonrootuser. - COPY reference — using
--chownto hand ownership to the target user. - WORKDIR reference — directory creation respects the current
USER. - Securing Docker builds tutorial — non-root containers in context.
- Security best practices for Docker images