Skip to main content
Docker advanced Lesson 8 of 9

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

ImageSizeShellUse when
ubuntu:22.0477MBbashDebugging, legacy apps
debian:slim75MBbashMost general apps
alpine:3.197MBshMost apps — good default
distroless/nodejs~60MBnoneProduction Node.js
scratch0MBnoneStatically 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.