Docker Security
Harden Docker images and containers: non-root users, read-only filesystems, secrets management, image scanning, and runtime policies.
Principle of Least Privilege
Run as Non-Root
FROM node:18-alpine
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --only=production
COPY --chown=node:node . .
# Switch to the node user (uid 1000, already exists in node images)
USER node
CMD ["node", "src/index.js"]
For custom users:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Read-Only Root Filesystem
docker run --read-only \
--tmpfs /tmp \ # allow writes to /tmp only
--tmpfs /var/run \
myapp:1.0
In Compose:
services:
api:
image: myapp:1.0
read_only: true
tmpfs:
- /tmp
- /var/run
Drop Capabilities
Linux capabilities grant root-like privileges individually. Drop all, add only what’s needed:
docker run \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \ # only needed capability
myapp:1.0
# docker-compose.yml
services:
api:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
Secrets Management
Docker Secrets (Swarm mode)
echo "supersecret" | docker secret create db_password -
docker service create \
--secret db_password \
--env DB_PASSWORD_FILE=/run/secrets/db_password \
myapp:1.0
tmpfs for Secrets at Build Time
Secrets needed only during build (SSH keys, npm tokens):
# syntax=docker/dockerfile:1
FROM node:18-alpine AS builder
# Mount secret at build time — not stored in any layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
docker build --secret id=npmrc,src=.npmrc -t myapp:1.0 .
Never Do This
# BAD — secret baked into image layer, visible with docker history
ENV DB_PASSWORD=supersecret
RUN curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com
Minimal Base Images
| Image | Size | Shell | Use when |
|---|---|---|---|
ubuntu:22.04 | 77MB | bash | Debugging, legacy apps |
debian:slim | 75MB | bash | Most general apps |
alpine:3.19 | 7MB | sh | Most apps — good default |
distroless/nodejs | ~60MB | none | Production Node.js |
scratch | 0MB | none | Statically compiled binaries (Go, Rust) |
# Distroless Node.js
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM gcr.io/distroless/nodejs18-debian12
WORKDIR /app
COPY --from=builder /app .
CMD ["src/index.js"]
Image Scanning
# Scan with Docker Scout (built into Docker Desktop)
docker scout cves myapp:1.0
# Scan with Trivy (open source, widely used in CI)
trivy image myapp:1.0
# Scan for secrets accidentally baked into images
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
trufflesecurity/trufflehog:latest \
docker --image myapp:1.0
Seccomp and AppArmor
Docker applies a default seccomp profile that blocks ~44 dangerous syscalls. You can restrict further:
# Apply a custom seccomp profile
docker run --security-opt seccomp=./custom-seccomp.json myapp:1.0
# Disable seccomp entirely (not recommended)
docker run --security-opt seccomp=unconfined myapp:1.0
# Apply AppArmor profile
docker run --security-opt apparmor=docker-default myapp:1.0
No New Privileges
Prevents privilege escalation via setuid binaries:
docker run --security-opt no-new-privileges myapp:1.0
# docker-compose.yml
services:
api:
security_opt:
- no-new-privileges:true
Dockerfile Best Practices Summary
# Pin exact image digest instead of mutable tag
FROM node:18.19.0-alpine3.19@sha256:abc123...
# Minimize layers — chain RUN commands
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# Never store secrets in ENV
# Use --mount=type=secret for build-time secrets
# Drop to non-root before CMD
USER nonroot
# Use COPY, not ADD (ADD can fetch remote URLs and extract tarballs)
COPY config.json /app/config.json Frequently Asked Questions
Why shouldn't containers run as root?
If a process inside the container is exploited, running as root gives the attacker root capabilities on the host (depending on kernel version and configuration). Running as a non-root user limits the blast radius of a container escape.
How should I pass secrets to containers?
Never in ENV variables (they appear in docker inspect and logs). Use Docker secrets (Swarm), a secret manager (Vault, AWS Secrets Manager), or a tmpfs mount. For local dev, .env files mounted read-only are acceptable.
What is a distroless image?
An image with no shell, no package manager, and no OS tools — just your app and its runtime dependencies. Created by Google. Massively reduces attack surface because there's nothing for an attacker to use even if they get code execution.