Docker Security: The Complete Guide for 2026
Containers do not equal security. Docker provides process isolation and filesystem namespacing, but a default docker run launches a root-privileged process with full Linux capabilities, shared kernel access, and no resource limits. One misconfigured container can compromise your entire host.
This guide covers every layer of Docker security — from choosing base images and writing hardened Dockerfiles, through runtime protections like seccomp and read-only filesystems, to supply chain integrity with SBOMs and image signing. Each section includes concrete commands and configurations you can apply today.
Table of Contents
- Why Container Security Matters
- Base Image Selection
- Multi-Stage Builds
- Running as Non-Root
- Rootless Docker
- Image Scanning
- Docker Content Trust and Image Signing
- Secrets Management
- Read-Only Filesystems
- Resource Limits
- Network Security
- Capabilities and Seccomp Profiles
- Docker Bench for Security
- CI/CD Security Pipeline
- Supply Chain Security
- Runtime Security Monitoring
- FAQ
1. Why Container Security Matters
Containers share the host kernel. Unlike virtual machines, which run their own OS kernel behind a hypervisor, every container on a host calls into the same Linux kernel. A kernel exploit from inside a container gives you the host. This is the fundamental security difference between containers and VMs.
The default Docker configuration is optimized for convenience, not security:
- Root by default — containers run as UID 0 unless you explicitly set a USER
- Full capabilities — Docker grants 14 Linux capabilities by default, including CAP_NET_RAW and CAP_CHOWN
- Writable filesystem — the container filesystem is read-write, allowing attackers to drop malware
- No resource limits — a container can consume all host CPU, memory, and disk I/O
- Docker socket access — mounting
/var/run/docker.sockgives full control of the Docker daemon (and the host)
Every section in this guide addresses one of these default weaknesses. Applied together, they create defense in depth: multiple layers that an attacker must bypass to cause damage.
2. Base Image Selection
Your base image determines your starting attack surface. Every package installed is a potential vulnerability. Choose the smallest image that supports your application.
# Attack surface comparison (approximate CVE counts):
# ubuntu:22.04 ~77MB ~200 packages ~50 CVEs
# python:3.12 ~900MB ~400 packages ~80 CVEs
# python:3.12-slim ~130MB ~100 packages ~20 CVEs
# python:3.12-alpine ~50MB ~30 packages ~5 CVEs
# gcr.io/distroless/python3 ~50MB minimal ~2 CVEs
# scratch 0MB 0 packages 0 CVEs
# Best choices by language:
# Go/Rust: scratch or distroless (static binaries)
# Python: python:3.12-slim or distroless
# Node.js: node:20-slim or distroless/nodejs
# Java: eclipse-temurin:21-jre-alpine or distroless/java21
Distroless images from Google contain only your application and its runtime dependencies — no shell, no package manager, no utilities. This means an attacker who gains code execution inside the container cannot spawn a shell, install tools, or pivot easily.
# Distroless example for a Go application
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
Always pin image digests for production. Tags are mutable — someone can push a compromised image to the same tag. Digests are immutable:
# Tag (mutable - can change without notice):
FROM node:20-alpine
# Digest (immutable - guaranteed to be the exact same image):
FROM node:20-alpine@sha256:1a526b97c5e5...
3. Multi-Stage Builds to Reduce Attack Surface
Multi-stage builds separate the build environment from the runtime environment. Your production image never contains compilers, build tools, dev dependencies, or source code.
# Stage 1: Build (has all dev tools - never shipped)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# Stage 2: Production (minimal runtime only)
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/package.json ./
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]
The builder stage might be 800MB with TypeScript, webpack, testing frameworks, and dev dependencies. The production stage is under 150MB with only the compiled output and production dependencies.
4. Running Containers as Non-Root
Running as root inside a container is the single most common Docker security mistake. If an attacker achieves remote code execution through your application, they inherit whatever user the container process runs as.
# Create a dedicated user in your Dockerfile
FROM python:3.12-slim
# Create non-root user and group
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --chown=appuser:appuser requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=appuser:appuser . .
# Switch to non-root BEFORE CMD
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"]
# Alpine uses different commands for user creation
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
# ...
USER app
# Enforce non-root at runtime (overrides Dockerfile USER)
docker run --user 1000:1000 myapp
# Verify the container is not running as root
docker exec mycontainer whoami
# Should output "appuser" or "1000", never "root"
# Prevent privilege escalation inside the container
docker run --security-opt=no-new-privileges myapp
5. Rootless Docker
Running containers as non-root is about the process inside the container. Rootless Docker goes further: the Docker daemon itself runs as an unprivileged user on the host. Even if an attacker escapes the container and compromises the daemon, they do not have root on the host.
# Install rootless Docker (requires uidmap package)
sudo apt-get install -y uidmap
dockerd-rootless-setuptool.sh install
# Configure your shell to use the rootless daemon
export PATH=/home/user/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock
# Verify rootless mode
docker info --format '{{.SecurityOptions}}'
# Should include "rootless"
# Rootless limitations:
# - Cannot bind to ports below 1024 (use port mapping above 1024)
# - Some storage drivers unavailable (overlay2 works with kernel 5.11+)
# - No --network host mode
# - Some volume mount restrictions
Rootless Docker is production-ready as of Docker Engine 20.10+ and is the recommended deployment mode for environments where maximum isolation is required.
6. Image Scanning
Image scanning detects known vulnerabilities (CVEs) in OS packages and application dependencies inside your images. Scan on every build and fail the pipeline on critical findings.
# Trivy - fast, comprehensive, open source
trivy image myapp:latest
trivy image --severity CRITICAL,HIGH myapp:latest
trivy image --exit-code 1 --severity CRITICAL myapp:latest # fail CI
# Grype - from Anchore, fast and accurate
grype myapp:latest
grype myapp:latest --fail-on critical
# Snyk Container
snyk container test myapp:latest
snyk container test --severity-threshold=high myapp:latest
# Docker Scout (built into Docker CLI)
docker scout cves myapp:latest
docker scout quickview myapp:latest
# Scan a Dockerfile before building
trivy config --policy-namespaces builtin Dockerfile
# Catches: running as root, using latest tag, COPY instead of ADD, etc.
# GitHub Actions integration
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/myapp:${{ github.sha }}
exit-code: 1
severity: CRITICAL,HIGH
format: sarif
output: trivy-results.sarif
- name: Upload scan results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
7. Docker Content Trust and Image Signing
Docker Content Trust (DCT) uses digital signatures to verify image integrity and publisher identity. When enabled, Docker refuses to pull unsigned images.
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Now docker push signs images automatically
docker push myorg/myapp:v1.0.0
# Signing and pushing trust metadata...
# docker pull verifies signatures before pulling
docker pull myorg/myapp:v1.0.0
# Pull verified by signature
# Unsigned images are rejected
docker pull untrusted/image:latest
# Error: trust data unavailable
Cosign (from the Sigstore project) is the modern alternative to DCT, supporting keyless signing with OIDC identity providers:
# Sign an image with cosign (keyless, uses OIDC)
cosign sign myregistry.io/myapp:v1.0.0
# Verify a signed image
cosign verify myregistry.io/myapp:v1.0.0
# Sign with a key pair (for CI environments)
cosign generate-key-pair
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0
8. Secrets Management
Secrets in Docker images are permanent. Every ENV, COPY, and RUN instruction creates a layer that can be extracted with docker history or docker save. Once a secret is in an image layer, it is compromised.
What NOT to Do
# NEVER: Secret in ENV (visible in docker inspect, docker history)
ENV DATABASE_PASSWORD=s3cretP@ss
# NEVER: Secret copied into image (persists in layer)
COPY .env /app/.env
COPY credentials.json /app/credentials.json
# NEVER: Secret in build arg (visible in docker history)
ARG API_KEY=sk-1234567890
RUN curl -H "Authorization: Bearer $API_KEY" https://api.example.com
Build-Time Secrets with BuildKit
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
# Mount secret during build - never persisted in image layer
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
pip install --no-cache-dir -r requirements.txt
# Pass the secret at build time
docker build --secret id=pip_conf,src=./pip.conf -t myapp .
Runtime Secrets
# Option 1: Environment variables (minimum viable approach)
docker run -e DATABASE_URL="postgres://user:pass@db:5432/app" myapp
# Caveat: visible in docker inspect output
# Option 2: Docker Swarm secrets (mounted as files)
echo "s3cretP@ss" | docker secret create db_password -
docker service create --secret db_password myapp
# Inside container: cat /run/secrets/db_password
# Option 3: Secrets manager (Vault, AWS Secrets Manager, etc.)
# Application fetches secrets at startup from the manager
# Best approach: secrets never appear in Docker configuration
9. Read-Only Filesystems
A read-only root filesystem prevents attackers from writing malware, modifying binaries, or tampering with configuration files inside the container.
# Run with read-only filesystem
docker run --read-only myapp
# Most apps need some writable directories for temp files, logs, etc.
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /var/run:rw,noexec,nosuid,size=1m \
myapp
# Docker Compose
services:
api:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:size=64m
- /var/run:size=1m
volumes:
- app-logs:/var/log/app # named volume for logs
The noexec flag on tmpfs prevents executing binaries from that directory, and nosuid prevents setuid escalation. Combined with a read-only root filesystem, an attacker has no writable location from which to execute code.
10. Resource Limits
Without resource limits, a single container can consume all host resources — CPU, memory, disk I/O, and process table entries — causing denial of service for every other container and the host itself.
# Set memory and CPU limits
docker run -d \
--memory=512m \
--memory-swap=512m \
--memory-reservation=256m \
--cpus=1.5 \
--pids-limit=100 \
--ulimit nofile=1024:2048 \
myapp
# Docker Compose resource limits
services:
api:
deploy:
resources:
limits:
memory: 512M
cpus: '1.5'
pids: 100
reservations:
memory: 256M
cpus: '0.5'
# Prevent fork bombs
# --pids-limit=100 restricts the number of processes
# Without this, a fork bomb inside a container can crash the host
# Prevent disk abuse
# Use storage driver limits or disk quotas
docker run --storage-opt size=10G myapp
11. Network Security
Docker's default bridge network allows all containers to communicate with each other. In production, isolate services using custom networks so that only services that need to communicate can reach each other.
# BAD: All containers on default bridge can reach each other
docker run -d --name api myapp
docker run -d --name db postgres # api can reach db, but so can everything else
# GOOD: Custom networks with isolation
docker network create frontend
docker network create backend
# Web server: accessible from outside, can reach API
docker run -d --name web --network frontend -p 443:443 nginx
# API: connected to both frontend and backend
docker run -d --name api --network frontend myapp
docker network connect backend api
# Database: only on backend network (not reachable from web)
docker run -d --name db --network backend postgres
# CRITICAL: Never expose database ports to the host
# BAD: docker run -p 5432:5432 postgres
# GOOD: docker run --network backend postgres
# ACCEPTABLE: docker run -p 127.0.0.1:5432:5432 postgres
# Docker Compose network isolation
services:
web:
image: nginx
ports: ["443:443"]
networks: [frontend]
api:
build: .
networks: [frontend, backend]
db:
image: postgres:16-alpine
networks: [backend] # only API can reach the database
cache:
image: redis:7-alpine
networks: [backend]
networks:
frontend:
backend:
internal: true # no external/internet access
The internal: true flag on the backend network prevents containers on that network from reaching the internet, which blocks data exfiltration from a compromised database container.
12. Capabilities and Seccomp Profiles
Linux capabilities split root privileges into granular units. Instead of all-or-nothing root access, you can grant only the specific privileges your application needs.
# Docker grants 14 capabilities by default. Drop them all, add back only what you need:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Common capabilities and when you need them:
# NET_BIND_SERVICE - bind to ports below 1024
# CHOWN - change file ownership
# SETUID/SETGID - change process UID/GID (needed by some init systems)
# SYS_PTRACE - debugging (never in production)
# NET_RAW - raw sockets, ping (usually not needed)
# For most web applications:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
myapp
Seccomp Profiles
Seccomp (Secure Computing Mode) restricts which system calls a container can make. Docker's default seccomp profile blocks about 44 of the 300+ Linux syscalls. You can create a custom profile that allows only the syscalls your application actually uses.
# Run with Docker's default seccomp profile (already applied by default)
docker run --security-opt seccomp=default myapp
# Generate a custom seccomp profile by tracing syscalls
# Step 1: Run with logging to identify needed syscalls
docker run --security-opt seccomp=unconfined strace -f -o /tmp/trace myapp
# Step 2: Create a restrictive profile (JSON format)
# custom-seccomp.json:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read","write","open","close","stat","fstat",
"mmap","mprotect","munmap","brk","accept",
"bind","listen","socket","connect","sendto",
"recvfrom","epoll_wait","epoll_ctl","exit_group"],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Step 3: Apply the custom profile
docker run --security-opt seccomp=custom-seccomp.json myapp
13. Docker Bench for Security
Docker Bench for Security is an automated audit script that checks your Docker host and containers against the CIS Docker Benchmark — a comprehensive set of security recommendations from the Center for Internet Security.
# Run Docker Bench for Security
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
# Output categories:
# [PASS] 1.1 - Ensure a separate partition for containers exists
# [WARN] 2.1 - Ensure network traffic is restricted between containers
# [FAIL] 4.1 - Ensure a user for the container has been created
# [INFO] 5.1 - Ensure AppArmor profile is enabled
# Focus on fixing FAIL items first, then WARN items
# Some WARN items may be acceptable depending on your threat model
14. CI/CD Security Pipeline
Security must be automated into your build pipeline. Manual security reviews do not scale. Every image that reaches production should pass through automated gates.
# Complete secure CI/CD pipeline (GitHub Actions)
name: Secure Docker Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Lint Dockerfile for security issues
- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3
with:
dockerfile: Dockerfile
# Build the image
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
# Scan for vulnerabilities
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
exit-code: 1
severity: CRITICAL,HIGH
# Generate SBOM
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
# Sign the image
- name: Sign image with cosign
run: cosign sign --yes myregistry.io/myapp:${{ github.sha }}
# Push only if all checks pass
- name: Push to registry
run: docker push myregistry.io/myapp:${{ github.sha }}
Hadolint catches Dockerfile anti-patterns before they become security issues: running as root, using latest tags, using ADD instead of COPY, installing packages without pinning versions.
15. Supply Chain Security
Supply chain attacks target the build process itself: compromised base images, poisoned dependencies, tampered build tools. Protecting your supply chain means verifying every component that goes into your images.
Software Bill of Materials (SBOM)
# Generate SBOM with Syft
syft myapp:latest -o spdx-json > sbom.spdx.json
syft myapp:latest -o cyclonedx-json > sbom.cdx.json
# Docker's built-in SBOM generation
docker sbom myapp:latest
# Attach SBOM as a build attestation
docker buildx build \
--sbom=true \
--provenance=true \
-t myapp:latest \
--push .
Build Provenance
Build provenance records how an image was built: what source code, which builder, what build parameters. SLSA (Supply chain Levels for Software Artifacts) defines maturity levels for provenance.
# Enable provenance attestations with BuildKit
docker buildx build \
--provenance=mode=max \
-t myapp:latest \
--push .
# Verify provenance of a pulled image
cosign verify-attestation \
--type slsaprovenance \
myregistry.io/myapp:latest
# Pin dependencies by digest in Dockerfile
FROM python:3.12-slim@sha256:abc123...
COPY requirements.txt .
# Use pip hash checking for Python dependencies
RUN pip install --no-cache-dir --require-hashes -r requirements.txt
16. Runtime Security Monitoring
Build-time security prevents known vulnerabilities from reaching production. Runtime security detects unexpected behavior after deployment — zero-day exploits, misconfigurations, and compromised containers.
# Falco - cloud-native runtime security (CNCF project)
# Detects unexpected behavior in real-time:
# - Shell spawned in container
# - Sensitive file read (/etc/shadow, /etc/passwd)
# - Unexpected network connection
# - Binary executed that was not in the original image
# - Privilege escalation attempts
# Install Falco
helm install falco falcosecurity/falco \
--set falcosidekick.enabled=true \
--set falcosidekick.config.slack.webhookurl=https://hooks.slack.com/...
# Example Falco rule: detect shell in container
- rule: Terminal shell in container
desc: Detect a shell being spawned in a container
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, dash)
output: >
Shell spawned in container
(user=%user.name container=%container.name shell=%proc.name)
priority: WARNING
# Docker events monitoring (built-in)
docker events --filter type=container --filter event=exec_start
# Monitor for suspicious activity patterns:
# - exec into production containers (someone poking around)
# - containers running as root
# - containers with privileged mode
# - new containers from unknown images
# Audit Docker daemon with auditd
sudo auditctl -w /usr/bin/docker -p rwxa -k docker-commands
sudo auditctl -w /var/lib/docker -p rwxa -k docker-filesystem
sudo auditctl -w /var/run/docker.sock -p rwxa -k docker-socket
Frequently Asked Questions
Why should Docker containers run as non-root?
Running containers as root means that if an attacker exploits a vulnerability in your application and escapes the container, they gain root access to the host system. Non-root containers limit the blast radius of a compromise: the attacker is confined to an unprivileged user even if they break out. Most applications do not need root privileges to function, so adding a USER directive in your Dockerfile is a simple change that dramatically improves your security posture. Combine this with --security-opt=no-new-privileges to prevent any process inside the container from gaining additional privileges.
What is the difference between rootless Docker and running containers as non-root?
Running containers as non-root (USER directive in Dockerfile) means the process inside the container runs as a non-root user, but the Docker daemon itself still runs as root on the host. Rootless Docker goes further: the entire Docker daemon runs as an unprivileged user. Even if an attacker escapes the container AND exploits the Docker daemon, they still do not have root on the host. Rootless Docker provides defense in depth and is recommended for production environments requiring maximum isolation. The trade-off is some limitations: no binding to ports below 1024, no --network host mode, and some storage driver restrictions.
How do I scan Docker images for vulnerabilities?
Use open-source scanners like Trivy (trivy image myapp:latest), Grype (grype myapp:latest), or Docker Scout (docker scout cves myapp:latest). Integrate scanning into your CI/CD pipeline and configure the scanner to fail the build on critical or high-severity CVEs. Scan on every build, not just periodically, because new vulnerabilities are disclosed daily. For comprehensive coverage, scan both OS packages and application dependencies. Upload results in SARIF format to GitHub Security or your vulnerability management platform for centralized tracking.
How should I manage secrets in Docker containers?
Never bake secrets into Docker images using ENV or COPY instructions — they persist in image layers and are visible via docker history. For build-time secrets, use BuildKit's --mount=type=secret which makes secrets available during build without persisting them. For runtime, the best approach is a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager) where the application fetches secrets at startup. Docker Swarm secrets mount secrets as files at /run/secrets/. As a minimum, pass secrets via runtime environment variables (docker run -e), but note they are visible in docker inspect output.
What is a Software Bill of Materials (SBOM) and why does it matter?
An SBOM is a complete inventory of every software component inside your Docker image — OS packages, application libraries, and transitive dependencies. When a critical vulnerability like Log4Shell is disclosed, an SBOM lets you instantly identify which images are affected without rescanning everything. Generate SBOMs with Syft (syft myapp:latest -o spdx-json) or Docker's built-in docker sbom command. Attach SBOMs as build attestations alongside your images. SBOMs are increasingly required for regulatory compliance, government procurement (US Executive Order 14028), and enterprise security audits.
Security Checklist
Apply these practices systematically to harden your Docker environment:
- Images: Use minimal base images (slim, Alpine, distroless). Pin versions by digest. Rebuild regularly to pick up security patches.
- Dockerfiles: Multi-stage builds. Non-root USER. No secrets in layers. BuildKit secret mounts for build-time credentials.
- Runtime: Read-only filesystem. Drop all capabilities, add only what is needed. Resource limits (memory, CPU, PIDs). No privilege escalation.
- Network: Custom networks with isolation. Internal networks for backend services. Never expose database ports publicly.
- Scanning: Trivy/Grype in CI/CD. Fail on critical CVEs. Hadolint for Dockerfile linting.
- Supply chain: Image signing with cosign. SBOM generation. Build provenance attestations.
- Monitoring: Falco for runtime detection. Docker events auditing. Regular Docker Bench audits.
Container security is not a single configuration change. It is a layered approach where each control compensates for the potential failure of another. Start with the highest-impact items — non-root users, image scanning, and secrets management — then progressively add capabilities dropping, seccomp profiles, and runtime monitoring as your security maturity grows.
Continue Learning
- Docker: The Complete Developer's Guide for 2026 — comprehensive Docker guide from basics to advanced topics
- Docker Compose: The Complete Guide — multi-container orchestration with security-focused configurations
- Kubernetes: The Complete Guide — container orchestration at scale with built-in security primitives