Run as Non-Root & Harden the Container
Containers run as root by default; create a dedicated user, drop privileges, and lock the container down so a breakout has nowhere to go.
By default your process runs as root inside the container, and container root maps to real capabilities on the host. If your app is ever popped, you have handed the attacker the keys. Running as an unprivileged user is the single highest-value hardening step you can take.
Create a real user in the image
Add a non-root user with USER in the Dockerfile. Make sure the directories your app writes to are owned by that user, or it will crash on permission errors.
Runtime hardening flags
--user— override the user at run time.--read-only— mount the root filesystem read-only; add--tmpfs /tmpfor scratch space.--cap-drop=ALLthen add back only what you need with--cap-add.--security-opt=no-new-privileges— block setuid privilege escalation.
Least privilege, always
Most web apps need zero Linux capabilities. Drop them all and see if anything breaks — usually nothing does.
Example
FROM node:20-alpine
# Create an unprivileged user and own the app dir
RUN addgroup -S app && adduser -S -G app -u 10001 app
WORKDIR /app
COPY --chown=app:app package*.json ./
RUN npm ci --omit=dev
COPY --chown=app:app . .
# Drop to the non-root user for everything that follows
USER app
EXPOSE 3000
CMD ["node", "server.js"]
# Run it hardened:
# docker run --read-only --tmpfs /tmp \
# --cap-drop=ALL --security-opt=no-new-privileges \
# -p 3000:3000 myapp:1.0When to use it
- A platform team enforces a policy that all containers must declare a non-root USER, validated by a CI hadolint check.
- A developer drops all Linux capabilities and adds back only what the process needs, so a compromised container cannot use ptrace.
- An ops engineer uses --security-opt no-new-privileges to prevent a setuid binary inside a container from escalating to root.
More examples
Alpine: create user and drop to it
Creates a system user and group on Alpine, sets file ownership, and switches to the non-root user before CMD.
FROM node:20-alpine
RUN addgroup -S appgrp && adduser -S appuser -G appgrp
WORKDIR /app
COPY --chown=appuser:appgrp package*.json ./
RUN npm ci --omit=dev
COPY --chown=appuser:appgrp . .
USER appuser
CMD ["node", "index.js"]Drop capabilities and add no-new-privileges
Combines cap-drop, no-new-privileges, and read-only filesystem for defence-in-depth container hardening.
docker run -d \
--cap-drop ALL \
--security-opt no-new-privileges \
--read-only \
--tmpfs /tmp \
myapp:latestVerify effective UID at runtime
Asserts that the container's effective user is not root, suitable for use as a CI gate.
docker run --rm myapp:latest id
# Expected output: uid=1001(appuser) gid=1001(appgrp)
# Fail CI if container runs as root
[ $(docker run --rm myapp:latest id -u) != '0' ] && echo OK || echo FAIL
Discussion