USER reference

Last reviewed on 2026-09-25

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.

Quick answer

Create a user with a fixed UID, then switch to it with USER after the steps that need root:

RUN groupadd --system --gid 10001 app \
 && useradd --system --uid 10001 --gid app --no-create-home app
USER 10001:10001

To open a shell in a running container as a different user (name, UID, or UID:GID):

docker exec -it -u <user> <container> sh
docker exec -it -u www-data my-php sh   # existing user
docker exec -it -u 0 my-app sh          # root, for debugging

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 avoids the interactive password prompt, and there is no --create-home — --home creates the directory on its own (pass -H to skip it). 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

In Compose the equivalents are user: "10001:10001" on the service and docker compose exec -u www-data <service> sh. With docker exec -u, a name must exist in the container's /etc/passwd (otherwise: unable to find user www-data: no matching entries in passwd file); a numeric UID works even if it has no entry.

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.

Switching to an existing user (www-data, nginx, node)

Many official images already contain a non-root account, so you do not need useradd — just hand it the files and switch:

ImageExisting userUID:GID
php:*-fpm, php:*-apache (Debian)www-data33:33
php:*-fpm-alpinewww-data82:82
nginx, nginxinc/nginx-unprivilegednginx101:101
nodenode1000:1000
FROM php:8.3-fpm
COPY --chown=www-data:www-data . /var/www/html
# Equivalent to "USER www-data", but numeric so it passes DL3066 / runAsNonRoot
USER 33:33

Check the UID in the exact tag you use rather than trusting a table: docker run --rm --entrypoint id php:8.3-fpm www-data. Two service-specific notes:

Linter and scanner warnings: DL3066 and "might run with root"

If the process genuinely has to start as root (for example an entrypoint that fixes volume permissions and then drops privileges with gosu or su-exec), mark the finding as reviewed rather than silencing it globally.

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. With BuildKit, a WORKDIR creates missing directories owned by the current user, so when it runs after USER it produces a writable directory. (The legacy builder created it as root; add an explicit chown if you still use it.)

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?

FAQ

How do I add a non-root user in a Dockerfile?

Create the user with a fixed numeric UID, then switch to it with USER after the steps that need root: RUN groupadd --system --gid 10001 app && useradd --system --uid 10001 --gid app app, followed by USER 10001:10001. On Alpine use addgroup -S -g 10001 app && adduser -S -u 10001 -G app app. See the worked example.

How do I connect to a running container as a user other than root?

Use docker exec with -u (or --user): docker exec -it -u www-data my-container sh. The value can be a name, a UID, or UID:GID. A name must exist in the container's /etc/passwd; a numeric UID does not have to. With Compose, use docker compose exec -u www-data <service> sh. To get a root shell in a non-root image, use -u 0.

How do I run a container as a different user without changing the Dockerfile?

Pass --user at run time: docker run --user 10001:10001 myimage, or docker run --user "$(id -u):$(id -g)" to match your host user on a bind mount. In Compose, set user: "10001:10001" on the service; in Kubernetes, set securityContext.runAsUser. These override the image's USER.

What does DL3066 "non-numeric user-id may not be resolvable by host system" mean?

The linter found USER followed by a name (for example USER app or USER www-data). A name only resolves through the image's /etc/passwd, so the host, Kubernetes runAsNonRoot and admission policies cannot verify it. Replace it with the numeric UID:GID, for example USER 10001:10001, or USER 33:33 for www-data on Debian-based images.

Why does my scanner say "this image might run with root as the default user"?

The final stage of the Dockerfile has no USER instruction (or sets USER root), and the scanner cannot prove the base image switches to a non-root user, so the container would start as UID 0. Add a USER with a non-zero numeric UID as the last user change in the final stage. Details below.