← All research

Do popular container images drop root? We checked 25 projects

Half of the primary Dockerfiles in 25 popular infra and app projects don't set an explicit non-root USER in their final stage. A fair, caveated look at what static analysis can and can't tell you about container privilege.

July 21, 2026 4 min readBy Zennoxa Researchcontainersdockerdockerfiledevsecops

TL;DR — Across 25 popular infrastructure and application projects, we looked at whether their Dockerfiles set a non-root USER. Of the 16 projects with a clear primary image, exactly half (8) don't set an explicit non-root USER in the final build stage. But "no USER line" is not the same as "insecure" — this post is as much about that distinction as about the number.

Why a non-root USER matters (and when it doesn't)

If a container's final image doesn't drop to a non-root user, a process that breaks out of the app runs as root inside the container — a bigger blast radius if it then finds a kernel or runtime escape. Setting USER app (a non-root uid) is the documented defense-in-depth default in Docker's and CIS's hardening guidance.

But three honest caveats mean "no USER line" ≠ "vulnerable", and we lead with them because a benchmark that hides its limits isn't one you should trust:

  1. A static check can't see into base images. FROM some/image that already sets a non-root USER internally is invisible to a Dockerfile scanner — so a file with no USER may still run non-root at runtime.
  2. It can't see runtime privilege drops. Some images stay root in the Dockerfile but drop to a non-root user at startup via gosu/su-exec/an entrypoint (gitea does exactly this). The container isn't actually running as root.
  3. Minimal images blur it. A static binary on scratch/distroless/alpine often runs as root with a tiny attack surface, where "root" means much less.

So the precise, checkable claim here is "the Dockerfile does not set an explicit non-root USER" — a defense-in-depth smell, not a proof of vulnerability. With that framing set, here's what we found.

What we measured

Full per-repo data in data.json.

The numbers

Cut No non-root USER Share
Primary image only (16 repos) 8 50.0%
All Dockerfiles (195 files) 110 56.4%

The "all Dockerfiles" figure is inflated by dev/test fixtures (local databases, proxies) where root is irrelevant — which is exactly why we don't lead with it. The primary-image cut is the fair headline: half of these projects' main Dockerfiles don't set a non-root USER.

Primary image sets a non-root USER: prometheus, vault, consul, posthog, mastodon, airflow, superset, netdata.

Primary image does not (final stage): grafana, moby, traefik, minio, appwrite, home-assistant, gitea, gogs — noting that some of these (e.g. gitea) drop privileges at runtime instead, per caveat #2.

What to actually do

Setting an explicit non-root user is a one-line change and removes the ambiguity — your image is provably non-root regardless of base-image behavior:

# create + switch to a non-root user in the FINAL stage
RUN adduser -D -u 10001 app
USER app

If you rely on a runtime drop (gosu) or a base image's user, that's defensible — but it's worth making explicit so a reviewer (or a scanner) can see it. Zennoxa Shield's CONTAINER-012 flags final stages with no non-root USER if you want it visible in CI.

Limitations

Reproduce it

git clone --depth 1 --filter=blob:none --no-checkout https://github.com/traefik/traefik t
cd t && git sparse-checkout init --no-cone && git sparse-checkout set '**/Dockerfile' && git checkout
# a Dockerfile whose final stage has no `USER` (non-root) defaults to root:
grep -nE '^\s*(FROM|USER)\b' Dockerfile

Per-repo counts and method in data.json.


Zennoxa Research publishes reproducible security data on public projects with public tooling. No third-party scanner is involved.

Scan your repo free More research
Do popular container images drop root? We checked 25 projects — Zennoxa Shield