How do I secure a Docker container?

Direct answer

Run as a non-root user, start from a minimal base image, and keep secrets out of the image entirely. Containers default to root, which means a container escape starts with root on the host. Three lines in your Dockerfile fix most of it.

Muhammad HasanUpdated

Don't run as root

RUN addgroup -S app && adduser -S app -G app
USER app

The default is root, and root inside the container maps to root on the host under most configurations. Any escape starts from the strongest possible position.

Add USER before the final CMD. If your app needs to bind a low port, front it with a proxy rather than granting the capability.

Minimal base images

node:20 is around a gigabyte of Debian, most of which is a shell, a package manager and utilities your application never uses. But an attacker with code execution does.

node:20-slim, -alpine, or a distroless image cut that considerably. Distroless has no shell at all, which removes most post-exploitation tooling.

Pin by digest rather than tag. node:20-alpine changes underneath you; node:20-alpine@sha256:... doesn't.

Secrets don't belong in images

# Wrong — baked into the layer, readable by anyone who pulls
ARG API_KEY
ENV API_KEY=$API_KEY

Layers are permanent. Deleting a file in a later layer doesn't remove it from the earlier one, and docker history will show it.

Inject at runtime through environment variables or a secret manager. For build-time credentials, use BuildKit secret mounts, which don't persist into the image.

Multi-stage builds

FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
RUN npm ci --omit=dev
USER node
CMD ["node", "dist/index.js"]

Build tools, source and dev dependencies stay in the first stage. The shipped image contains only what runs.

Runtime restrictions

docker run \
  --read-only \
  --cap-drop=ALL \
  --security-opt=no-new-privileges \
  myapp

Read-only filesystem with a tmpfs mount for anything that needs writing. Drop all capabilities and add back only what's needed, most applications need none.

Never --privileged, and never mount /var/run/docker.sock into a container you don't fully trust. Access to the Docker socket is equivalent to root on the host.

Scanning

docker scout, Trivy or Grype will list known CVEs in your image layers. Run it in CI and fail on critical findings in the base image.

Most results come from the base image rather than your code, which is the strongest argument for the minimal image: fewer packages, fewer CVEs, less triage.

Go deeper

Kolega for DevOps

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free