← News
/PLAYBOOK · 2026-08-06

Docker Container Security Hardening: Image and Runtime Controls

Containers are not a security boundary. Default Docker configs allow privilege escalation, escape, and resource exhaustion. Hardening checklist: minimal images, non-root runtime, dropped capabilities, and a read-only filesystem.

Docker container security hardening is baseline work before you ship cloud-native apps. Containers share the host kernel. By default, in-container processes run as root with a full capability set. If the app is compromised or an escape vuln exists (CVE-2022-0492, CVE-2026-series kernel vulns), an attacker reaches the host. This article gives a copy-paste plan across image build, runtime privileges, and resource limits.

Scope

Prerequisites

How it works

Container risk concentrates in four layers:

Hardening follows least privilege, least image, least capability: non-root, drop capabilities, read-only FS, resource limits, plus image scanning to block vulnerable bases.

Steps

1. Minimal image build (multi-stage)

# Dockerfile example: multi-stage build + non-root user
# Stage 1: build (full toolchain)
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime (artifacts and runtime only)
FROM node:20-alpine
# Create non-root user; do not run as root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev --ignore-scripts && chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Key points: `USER appuser` is non-root; `--omit=dev` skips dev deps; alpine is small with a thin toolchain. Scan immediately after build with `docker scan` or Trivy.

2. Image security scanning

# Install Trivy (lightweight vulnerability scanner)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan local image (base image and dependency vulns)
trivy image your-image:latest
# HIGH+ only, JSON for CI
trivy image --severity HIGH,CRITICAL --format json your-image:latest
# Fail CI on HIGH+
trivy image --exit-code 1 --severity HIGH,CRITICAL your-image:latest

3. Runtime: drop capabilities and privileges

# docker run example: drop all capabilities, add back as needed
docker run -d --name app   --cap-drop=ALL   --cap-add=NET_BIND_SERVICE   --security-opt=no-new-privileges   --read-only   --tmpfs /tmp   -p 3000:3000   your-image:latest
# docker-compose.yml equivalent
services:
  app:
    image: your-image:latest
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs: [/tmp]

Params: `--cap-drop=ALL` removes all kernel capabilities; `NET_BIND_SERVICE` allows binding low ports (80/443). `--read-only` makes the root FS read-only; writes go to `--tmpfs`. `no-new-privileges` blocks privilege elevation (setuid is a no-op).

4. Resource limits and security params

docker run -d --name app   --memory=512m --memory-swap=512m   --cpus=1.0 --pids-limit=256   --restart=on-failure:5   your-image:latest
# compose equivalent
services:
  app:
    image: your-image:latest
    mem_limit: 512m
    memswap_limit: 512m
    cpus: 1.0
    pids_limit: 256

Set `memory-swap` equal to `memory` to disable swap and reduce escape surface. `pids-limit` caps process count against fork bombs.

5. Never mount sensitive paths

# Wrong (never do this):
# docker run -v /:/host your-image        # mount host root
# docker run -v /var/run/docker.sock:/var/run/docker.sock your-image  # Docker socket = full host control
# Correct: mount only what the app needs, read-only
docker run -d --name app   -v /data/config:/app/config:ro   your-image:latest

6. Enable default seccomp and AppArmor

# Docker ships a default seccomp profile; confirm it is not disabled
docker run -d --name app   --security-opt seccomp=default.json   your-image:latest
# Inspect default seccomp config
docker info --format '{{.SecurityOptions}}'
# Expected output includes name=seccomp and name=apparmor

Verification

# 1. Confirm in-container user is non-root
docker exec app id
# Expected: uid=1000(appuser), not uid=0(root)
# 2. Confirm capabilities are dropped
docker exec app capsh --print 2>/dev/null | grep Current
# Expected: only allowlisted caps such as net_bind_service
# 3. Confirm read-only filesystem
docker exec app touch /root-test.txt
# Expected: read-only file system error
# 4. Confirm resource limits
docker inspect app --format '{{.HostConfig.Memory}}'  # 536870912 (512m)
# 5. Image vuln scan
trivy image --severity HIGH,CRITICAL your-image:latest

FAQ

Q1: After cap-drop=ALL the container fails to start or misbehaves?

Usually a missing capability. Add from the error: low-port bind needs `NET_BIND_SERVICE`; packet capture/diagnostics need `NET_RAW`; writing logs needs `CHOWN`/`DAC_OVERRIDE`, not `SYS_ADMIN`. In a test env, `cap-add` one at a time until the app works, then record the allowlist. If the app needs a full cap set, split the service or switch to a runtime such as sysbox.

Q2: After read-only the app cannot write logs/cache?

Mount writable paths as tmpfs or volumes: logs `--tmpfs /var/log`, cache `--tmpfs /tmp`; persistent data with a named volume `-v appdata:/data`. `--tmpfs` is not persistent; it clears on restart—fine for log/cache.

Q3: How to check running containers for compliance?

Use Docker Bench for Security (official baseline checker):

# Run on the host (needs docker.sock access)
docker run -it --net host --pid host --userns host   --cap-add audit_control   -e DOCKER_CONTENT_TRUST=1   -v /var/lib:/var/lib -v /var/run/docker.sock:/var/run/docker.sock   docker/docker-bench-security
# Output is PASS/WARN/FAIL per check; remediate WARN/FAIL items

Takeaway

Docker hardening is minimal image, non-root, dropped capabilities, resource limits. Multi-stage builds plus vuln scan. `USER` non-root cuts privilege-escalation paths. `--cap-drop=ALL` + `--read-only` shrink kernel caps and write surface. Memory/CPU/PID limits against resource abuse. Never mount Docker socket or other sensitive paths. Freeze this as org-level image templates and compose standards; run Docker Bench on a schedule. No silver bullet—defense in depth plus continuous scanning.