Skip to main content

Docker Non-Root

The practice of configuring Docker containers to run as a non-root user (UID > 0) to limit the damage a compromised container process can do to the host system and other containers.

Docker Non-Root — Running Containers Without Root Privileges

What Is Docker Non-Root in Simple Terms?

By default, every Docker container runs as root (UID 0). This means if an attacker exploits a vulnerability in your application and gets code execution inside the container, they have root privileges inside the container. While container isolation limits what they can do, running as root dramatically increases the potential damage.

Running as a non-root user is one of the most impactful security improvements you can make to any Docker container.

TEXT
Root container exploited:
Attacker has UID 0 inside container
Can read all files in the container
Can modify /etc/passwd, install tools
Higher chance of privilege escalation to host
All setuid binaries executable
Non-root container exploited:
Attacker has UID 1001 inside container
Cannot read root-owned files
Cannot install system packages
Cannot execute setuid binaries
Much harder to escalate privileges

Adding Non-Root User in Dockerfile

Dockerfile
# Method 1 — Create a new user (most explicit)
FROM node:20-alpine
WORKDIR /app
# Create group and user
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
# Give ownership to non-root user
RUN chown -R appuser:appgroup /app
# Switch to non-root before CMD
USER appuser
EXPOSE 8080
CMD ["node", "dist/server.js"]
Dockerfile
# Method 2 — Use the pre-existing node user (node:20 images include it)
FROM node:20-alpine
WORKDIR /app
# Copy with correct ownership from the start
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node dist/ ./dist/
# Switch to node user (UID 1000, already exists in node images)
USER node
CMD ["node", "dist/server.js"]

Verifying Non-Root

Bash
# Build and verify
docker build -t myapp:latest .
docker run --rm myapp:latest whoami
# appuser <- not root
docker run --rm myapp:latest id
# uid=1001(appuser) gid=1001(appgroup)
# What user is a running container using?
docker inspect payment-api \
--format '{{.Config.User}}'
# appuser
# Check existing containers for root
docker ps -q | xargs docker inspect \
--format '{{.Name}}: {{.Config.User}}' \
| grep -v "appuser\|node\|1000\|1001"
# Any empty result = running as root

File Permission Issues

Dockerfile
# Common issue: non-root user cannot write to a directory
# Solution: set correct ownership before switching user
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
# Create directories the app needs to write to
RUN mkdir -p /app/logs /app/uploads && \
chown -R node:node /app/logs /app/uploads
USER node
CMD ["node", "server.js"]
Remember

Set the USER instruction in your Dockerfile before CMD or ENTRYPOINT. The process that starts the container runs as this user. If you set USER after CMD, the CMD runs as the user that was active before USER — which is root.

Security

Even with a non-root USER, if you mount the Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) into the container, any user inside that container can control Docker on the host and effectively has root access. Non-root inside the container provides real protection only when combined with not mounting sensitive host resources.

Frequently Asked Questions

Why does it matter if a container process runs as root when the container is already isolated by namespaces?

Namespace isolation is not a hard security boundary — container escape vulnerabilities (kernel bugs, misconfigured capabilities, Docker socket exposure) have been found repeatedly over the years, and when one is exploited, a process running as UID 0 inside the container often maps to root-equivalent privileges on the host or at least far more capability than a non-root user would have. Running as non-root is defense in depth: it doesn't stop every attack, but it meaningfully narrows what a successful one can do.

What's the common mistake teams make when trying to add USER to an existing Dockerfile?

Adding `USER 1000` without first ensuring that user has write access to directories the app needs at runtime (log paths, temp directories, mounted volumes) — the container then crashes on startup with permission errors that look unrelated to the USER change. The fix is setting ownership explicitly with `chown` during the build (while still root, before the USER instruction switches identity) rather than trying to fix permissions after the fact at runtime.