Docker Security: The Complete Guide for 2026

Published February 12, 2026 · 28 min read

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.

⚙ Build secure Dockerfiles: Use our Dockerfile Builder to generate hardened Dockerfiles with non-root users, multi-stage builds, and security best practices built in.

Table of Contents

  1. Why Container Security Matters
  2. Base Image Selection
  3. Multi-Stage Builds
  4. Running as Non-Root
  5. Rootless Docker
  6. Image Scanning
  7. Docker Content Trust and Image Signing
  8. Secrets Management
  9. Read-Only Filesystems
  10. Resource Limits
  11. Network Security
  12. Capabilities and Seccomp Profiles
  13. Docker Bench for Security
  14. CI/CD Security Pipeline
  15. Supply Chain Security
  16. Runtime Security Monitoring
  17. 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:

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:

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.

⚙ Build secure Dockerfiles: Use the Dockerfile Builder to generate production-ready Dockerfiles with non-root users, multi-stage builds, and security best practices applied automatically.

Continue Learning

Related Resources

Dockerfile Builder
Generate secure, production-ready Dockerfiles
Docker Complete Guide
Master Docker from basics to advanced patterns
Docker Compose Guide
Multi-container orchestration and configuration
Kubernetes Complete Guide
Container orchestration with security primitives
Docker Commands Cheat Sheet
Quick reference for all Docker commands
GitHub Actions CI/CD Guide
Automate secure Docker builds in your pipeline