Dockerfile Non-Root USER Generator

Add a non-root user properly. The two things that make it work are the numeric form and getting the ownership right at COPY time.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Generate. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

An Alpine base

adduser rather than useradd, and the USER line is numeric

FROM node:20-alpine
WORKDIR /app

A distroless base

Already has a nonroot user, and there is no shell to create one with

FROM gcr.io/distroless/static

A USER already present

Worth checking it is the last one, and after everything that writes to system paths

FROM debian:12
USER app
RUN apt-get install -y curl

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Writing USER appuser rather than USER 10001

    Kubernetes needs a number to enforce runAsNonRoot and rejects the pod when it sees a name. The name also fails on a base with no /etc/passwd.

    Instead:Use the numeric form. Create the named user as well if it helps readability.

  2. Copying files before switching user

    They are owned by root, so the application cannot write where it needs to and fails at run time in a container that built cleanly.

    Instead:Use COPY --chown=UID:GID rather than a RUN chown, which duplicates every file into a new layer.

  3. Putting the USER line near the top

    Everything below it runs as that user, so the package installs fail on permissions.

    Instead:Put it after the installs and the copies, near the end.

Kubernetes cannot enforce runAsNonRoot against a name

It has to see a number to know the user is not root, so a Dockerfile ending USER appuser is rejected by a pod with runAsNonRoot set, even though the user is not root.

The numeric form works everywhere

A name has to be resolved from /etc/passwd inside the image, which does not exist on a scratch or distroless base. A UID needs nothing. Keep the named user for the humans reading the file and write the number in the USER line.

Files copied before the USER line are owned by root

COPY writes as root, so switching user afterwards leaves the application unable to write to a cache directory, a socket path or a lock file. Use COPY --chown, which costs nothing, rather than a RUN chown afterwards, which duplicates every copied file into a new layer.

The USER line goes near the end

Everything below it runs as that user, including package installs, so a USER placed early makes the build fail on permissions. After the installs and the copies is almost always right.

A non-root process cannot bind a port below 1024

So an image that listened on 80 as root fails to start. Publishing still works, because the host side is bound by the daemon: change the application to 8080 and publish it as -p 80:8080.

The UID has to match a bind-mounted directory's owner

A bind mount keeps the host's ownership, so a container running as 10001 writing to a directory owned by 1000 gets permission denied. A named volume is different: Docker copies the image's ownership into it on first use, which is why the same setup works with a volume and not with a bind mount.

distroless already has one, and it is 65532

There is no shell, no adduser and no /etc/passwd to edit, so creating a user is neither possible nor needed. The :nonroot tag sets it; on the plain tag, USER 65532:65532 does the same.

More docker tools

Docker Container Image Reference Parser Is that a registry or a Docker Hub user? Dockerfile Linter What this image will do to you later docker run to Kubernetes YAML And the flags that have no equivalent Docker Compose to Kubernetes And why depends_on has no equivalent Docker Logging Driver Generator json-file never rotates Docker OCI Image Labels Generator One of them actually does something Docker Hub Pull Rate Limit Calculator Counted per IP, not per person Docker Registry Storage Calculator Deleting a tag frees nothing Docker Build Context Analyzer All of it uploads before anything runs Docker Compose to docker run Converter Names stop resolving Docker Compose Version Migrator version: is obsolete now Docker Compose Network and Port Viewer Everything reaches everything Docker Command Explainer Which flags hand over the host Docker BuildKit Cache Mount Generator apt deletes the cache you just mounted Docker BuildKit Secret Mount Generator ARG is in docker history forever Docker .dockerignore Pattern Tester *.log does not match logs/app.log Docker Image Manifest Viewer unknown/unknown is not broken Docker Image Config Viewer Anyone who can pull can read your Env Docker Registry Token Decoder Check the scope before the credentials Docker HEALTHCHECK Generator Docker does nothing when it fails Docker Network Subnet Calculator 31 networks, then it stops Docker History Analyzer The rm did not save anything Docker Run to Compose Converter Some flags have no equivalent Docker CMD and ENTRYPOINT Tester Why docker stop takes ten seconds Docker Port Mapping Tester -p reaches past your firewall Docker -v to --mount Converter -v invents missing directories Docker Restart Policy Simulator The backoff resets after ten seconds Docker Exit Code Explainer 137 is two different problems Docker CPU Limit Converter --cpu-shares limits nothing Docker Memory Limit Calculator -m 512m allows a gigabyte Docker Compose Interpolation Tester An unset variable is an empty string Docker Environment Variable Precedence Tester Your shell does not reach the container Docker Compose Override Merge Previewer An override can add a port, never remove one Dockerfile Multi-Stage Build Analyzer Which stages reach the image Dockerignore Validator It is not .gitignore

Elsewhere on the site