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.
  • :latest tag — surprises on rebuild.
  • Bake .env into 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 .