Learning paths / Containers / Writing Dockerfiles

Non-root users and least privilege in images

Reading · 5 min · Module 4, lesson 3 of 640 min left in this module

Module 4 · Writing DockerfilesLesson 3 of 6

Goal: Make an image run its app as a non-root user, and give it only the file access it needs.

Key idea

Unless the image says otherwise, the app in a container runs as root. If someone finds a hole in your app, they get whatever that user can do. One USER line in the Dockerfile makes the app run as an ordinary user instead, so a break-in gets much less.

Root is the default

Run id in a plain Alpine container and you'll see the default:

docker run --rm alpine:3.22 id
uid=0(root) gid=0(root) groups=0(root),...

Root inside a container can change any file in it, install tools, and read anything the app can reach. It's also user 0 to the host's kernel, so a flaw that lets a process escape the container is far worse from root. Least privilege means the app gets the access its job needs and nothing more.

Switch user in the Dockerfile

Many base images ship a ready-made non-root user:

  • The official Node.js images have one called node: USER node.
  • Distroless :nonroot images have nonroot, user 65532: USER nonroot:nonroot.

When the base has none, create one, then switch to it after the steps that need root:

FROM alpine:3.22
RUN addgroup -S app && adduser -S -G app app
WORKDIR /app
COPY . .
USER app
CMD ["./server"]

Everything after USER runs as app, including the container's command. On Debian-based images the create line is RUN useradd --system app.

Check who an image runs as

Before you run someone else's image, or ship your own, ask it:

docker image inspect alpine:3.22 --format '{{.Config.User}}'
docker image inspect quay.io/computesphere/learn-hello-web:1.0.0 --format '{{.Config.User}}'

The first prints an empty line: no USER was set, so it runs as root. The second prints nonroot:nonroot, the user it starts as. docker image inspect reads images on your machine; if it says No such image, docker pull that image first. For a running container, docker exec <name> id answers the same question, as long as the image has an id command.

Files the app writes

Files copied into an image belong to root unless you say otherwise. An app running as app can read them but not change them. With a plain COPY, writing to one fails:

touch: /app/data.txt: Permission denied

COPY --chown=app:app makes the copied files the app user's. Keep that to the files the app must change: code it only reads is safer left owned by root.

More least-privilege switches at run time
  • docker run --user 1000:1000 ... runs as that user without changing the image; id inside shows uid=1000.
  • --read-only mounts the container's filesystem read-only. A write then fails with Read-only file system, which suits an app that keeps nothing on disk.
  • --cap-drop ALL removes the special powers the kernel otherwise grants the container's root user.

Check yourself

A Dockerfile ends with USER app, then RUN apk add curl. The build fails with a permission error. Why?
Your app runs as a non-root user and only reads its code. Which files should it own?