Safeguard
Security Guides•

How to run a container as a non-root user in a Dockerfile

Add a USER line with an explicit numeric UID, fix file ownership, and test that the app still starts. Here is the short path to a non-root container.

Safeguard Security Team
4 min read

To run a container as non-root, create an unprivileged user in the Dockerfile, give it an explicit numeric UID, make sure it owns only the files it must write, and finish with a USER instruction before the entrypoint. Then test the container, because apps that bind low ports or write to odd paths will fail.

Why bother

By default, the process inside a container runs as root unless the image says otherwise. Root inside a container is not the same as root on the host, but it is still more power than a web service needs. If an attacker gets code execution in the app, a non-root user limits what they can change inside the container and removes some escalation paths.

The OWASP Docker Security Cheat Sheet puts it directly: configuring the container to use an unprivileged user is the best way to prevent privilege escalation attacks. The Docker build guidance gives the same advice: if a service can run without privileges, use USER to change to a non-root user.

The minimal Dockerfile change

FROM debian:stable-slim
RUN groupadd --gid 10001 app \
 && useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin app
WORKDIR /app
COPY --chown=10001:10001 ./dist /app
USER 10001:10001
ENTRYPOINT ["/app/server"]

Three details matter here.

First, the UID is explicit. The Docker guidance says to consider an explicit UID and GID, because IDs assigned automatically can change between rebuilds. A stable number also makes volume permissions and cluster policies predictable.

Second, USER uses the number, not just the name. Orchestrators can check a numeric ID without looking inside the image's user database.

Third, ownership is set at copy time with the chown option on COPY, so you do not need a separate RUN chown layer that duplicates files.

What usually breaks

Most failures after the switch fall into a short list.

  • Low ports. A non-root process generally cannot bind ports below 1024 without extra privileges. Listen on 8080 and map the port outside the container.
  • Writes to the app directory. If the app writes caches or logs next to its code, give it a dedicated writable path, or mount a volume or temporary filesystem there.
  • Package installs at start. Anything that runs an installer at startup needs root. Move it into the build.
  • Mounted volumes. A volume created by the host may be owned by a different UID. Match the numbers rather than loosening permissions to world-writable.

Set it again at runtime

A USER line protects images you build. You can also enforce the choice where the container runs. Docker's run command accepts a user option that takes a name or UID, and Kubernetes security context lets you set runAsUser for a pod or a single container. Doing both means a rebuilt image that forgets the USER line does not silently go back to root.

Pair non-root with the other cheap controls: drop capabilities you do not need, set the no-new-privileges option, and use a read-only root filesystem. See the read-only filesystem guide for that last one.

How to verify

Run the container and ask which user it is. Override the entrypoint with a one-off command such as id, or inspect the process list from the host. If you see UID 0 for the app process, the USER line is missing, overridden, or placed before a later stage that resets it. In multi-stage builds, the USER instruction must be in the final stage.

Safeguard's SBOM and dependency scanning can look at what is inside the image you ship, and policies and gates can stop a build that does not meet your rules. Run a free scan to see what a current image contains.

Quick check

Is USER nobody good enough?

It works, but many images share that account, and its numeric ID can differ between base images. A dedicated user with a fixed UID is easier to reason about and to match with volume ownership.

Does non-root stop a container escape?

No. It reduces what a compromised process can do inside the container and blocks some escalation paths. Treat it as one layer next to dropped capabilities, seccomp, and patched hosts.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.