Docker security cheatsheet.
Non-root user
RUN useradd -u 1000 -m app
USER app
Or numeric:
USER 1000:1000
Always drop root in final image.
–user override
docker run -u 1000:1000 app
Drop capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE app
Default Docker drops most caps but keeps some. Tighten further.
–security-opt
docker run --security-opt no-new-privileges app
docker run --security-opt seccomp=default.json app
docker run --security-opt apparmor=docker-default app
Read-only root FS
docker run --read-only --tmpfs /tmp --tmpfs /var/run app
services:
web:
read_only: true
tmpfs: ["/tmp", "/var/run"]
No –privileged
# DANGER: full host access
docker run --privileged app
# Equivalent of "I trust this image with root"
Avoid. If you need a specific capability, add only that one.
Secrets
NEVER bake secrets into images:
# BAD
ENV API_KEY=sk-...
# BAD
COPY .env .env
Build-time secrets (BuildKit)
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=apikey \
cat /run/secrets/apikey > config
docker build --secret id=apikey,src=./apikey.txt .
Runtime secrets
docker run -e API_KEY=$(cat key.txt) app
docker run --env-file .env app
Or use Docker secrets / K8s secrets / vault / cloud provider.
Compose secrets
services:
web:
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txt
In container: /run/secrets/db_password.
Image scanning
# docker scout
docker scout cves myapp:v1
docker scout recommendations myapp:v1
# trivy
trivy image myapp:v1
trivy image --severity HIGH,CRITICAL myapp:v1
# grype
grype myapp:v1
Run in CI:
- run: docker scout cves myapp:${{ github.sha }} --exit-code --only-severity critical,high
Use minimal base images
Less code → less attack surface.
FROM alpine:3.20 # ~5MB
FROM debian:12-slim # ~50MB
FROM gcr.io/distroless/python3 # no shell, no pkgmgr
FROM scratch # empty (Go static binaries)
Pin versions:
FROM python:3.13.0-slim-bookworm@sha256:abc123...
Digest-pinned = reproducible + tamper-resistant.
Sign images (cosign)
cosign sign myregistry/myapp:v1
cosign verify myregistry/myapp:v1
.dockerignore (avoid leaking)
.env*
*.key
*.pem
.git
node_modules
__pycache__
Don’t expose docker socket
# DANGER: container can control host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Mounting docker.sock = root access to host. Use docker-out-of-docker (DooD) or socket-proxy.
Limit resources
docker run --memory=512m --cpus=1 --pids-limit=100 app
Prevent DoS from runaway containers.
Logging without secrets
log.info("login", user_id=u.id) # ok
log.info("login", password=pw) # NO
Sanitize structured logs.
Network segmentation
networks:
frontend:
backend:
internal: true # no external access
services:
web: { networks: [frontend, backend] }
db: { networks: [backend] } # not reachable from outside
Runtime monitoring
- Falco: detects suspicious syscalls.
- Tetragon: eBPF-based.
- Aqua: commercial.
Update + rotate
- Rebuild images regularly to get OS patches.
- Pin base image SHAs but update them monthly.
- Auto-PR with Dependabot / Renovate.
namespaces / cgroups (defense in depth)
Docker uses user namespaces by default? No. Enable explicitly:
// /etc/docker/daemon.json
{ "userns-remap": "default" }
Container UIDs map to non-root on host.
Common mistakes
- Running as root in production image.
:latesttag — surprises on rebuild.- Bake
.envinto image. - Mount docker socket without strict access control.
- Privileged container “just to make it work.”
- Outdated base image with known CVEs.
Read this next
If you want my security baseline + scan workflow, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .