Docker Security: Best Practices - Complete Production Guide

Docker container security is critical for protecting applications, data, and infrastructure. This comprehensive guide covers Docker security hardening, vulnerability management, runtime protection, and compliance best practices for production environments.

Table of Contents

Introduction

Container security requires a multi-layered approach covering images, runtime configuration, networks, secrets, and the Docker daemon itself. Proper security practices prevent unauthorized access, data breaches, and resource abuse.

Security Attack Surface

  • Images: Vulnerable dependencies and malware
  • Runtime: Privilege escalation and container breakout
  • Network: Unauthorized access and data interception
  • Secrets: Exposed credentials and API keys
  • Daemon: Docker socket exposure and misconfigurations

Security Principles

Least Privilege

Run containers with minimum necessary permissions:

# Don't run as root
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Defense in Depth

Multiple security layers:

  1. Secure base images
  2. Vulnerability scanning
  3. Runtime restrictions
  4. Network isolation
  5. Secrets encryption

Immutable Infrastructure

# Read-only root filesystem
docker run --read-only --tmpfs /tmp my-app

Image Security

Use Official Images

# Trusted sources only
FROM node:18-alpine  # Official Node image
FROM nginx:alpine    # Official Nginx image

# Avoid unknown sources
# FROM randomuser/suspicious-image  # Don't use

Pin Specific Versions

# Bad - unpredictable
FROM node:latest

# Good - reproducible
FROM node:18.17.0-alpine3.18

# Include SHA256 digest
FROM node:18.17.0-alpine3.18@sha256:abc123...

Minimize Image Size

# Smaller images = smaller attack surface
FROM alpine:3.18
# Or distroless
FROM gcr.io/distroless/static-debian11

Multi-Stage Builds

# Build stage with tools
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o app

# Minimal runtime
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/app /
ENTRYPOINT ["/app"]

Don't Include Secrets in Images

# Bad - secrets in image
ENV API_KEY=secret123
COPY .env /app/.env

# Good - inject at runtime
# docker run -e API_KEY=${API_KEY} my-app

Sign and Verify Images

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Build signed image
docker build -t my-image:latest .

# Push signed image
docker push my-image:latest

Container Runtime Security

Run as Non-Root User

# Create and use non-root user
FROM alpine:3.18
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
CMD ["./app"]
# Or specify at runtime
docker run --user 1000:1000 my-app

Read-Only Root Filesystem

# Prevent modifications to container filesystem
docker run -d \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /run \
  nginx
# Docker Compose
services:
  app:
    image: my-app
    read_only: true
    tmpfs:
      - /tmp
      - /run

Drop Capabilities

# Drop all capabilities, add only what's needed
docker run -d \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  nginx

Available capabilities:

  • NET_BIND_SERVICE: Bind to ports < 1024
  • CHOWN: Change file ownership
  • DAC_OVERRIDE: Bypass file permission checks
  • SETUID/SETGID: Change user/group ID

Disable New Privileges

# Prevent privilege escalation
docker run -d \
  --security-opt=no-new-privileges:true \
  my-app

Use Security Profiles

AppArmor

# Use AppArmor profile
docker run -d \
  --security-opt apparmor=docker-default \
  nginx

SELinux

# SELinux labels
docker run -d \
  --security-opt label=level:s0:c100,c200 \
  nginx

Seccomp

# Use seccomp profile
docker run -d \
  --security-opt seccomp=/path/to/seccomp-profile.json \
  my-app

Seccomp profile example:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "open", "close"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Limit System Resources

# Prevent DoS attacks
docker run -d \
  --memory="512m" \
  --memory-swap="512m" \
  --cpus="1.5" \
  --pids-limit=100 \
  my-app

Isolate Containers

# Use user namespaces
docker run -d --userns-remap=default my-app

Configure in /etc/docker/daemon.json:

{
  "userns-remap": "default"
}

Network Security

Network Isolation

# Create isolated networks
docker network create --driver bridge frontend
docker network create --driver bridge backend
docker network create --driver bridge database

# Connect containers to appropriate networks
docker run -d --name web --network frontend nginx
docker run -d --name api --network backend my-api
docker run -d --name db --network database postgres

Disable Inter-Container Communication

# Create network with ICC disabled
docker network create \
  --driver bridge \
  --opt com.docker.network.bridge.enable_icc=false \
  isolated-network

Encrypted Overlay Networks

# Create encrypted overlay network (Swarm)
docker network create \
  --driver overlay \
  --opt encrypted=true \
  secure-overlay

Restrict Container Network Access

# No network access
docker run -d --network none my-batch-processor

# Use network policies in Kubernetes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

TLS for Communication

# Configure Docker daemon for TLS
dockerd \
  --tlsverify \
  --tlscacert=ca.pem \
  --tlscert=server-cert.pem \
  --tlskey=server-key.pem \
  -H=0.0.0.0:2376

Secrets Management

Never Hardcode Secrets

# Bad
ENV DB_PASSWORD=mysecretpassword

# Good - pass at runtime
# docker run -e DB_PASSWORD=${DB_PASSWORD} my-app

Use Docker Secrets (Swarm)

# Create secret
echo "mypassword" | docker secret create db_password -

# Use in service
docker service create \
  --name app \
  --secret db_password \
  my-app

Access in container:

# Secret available at /run/secrets/db_password
cat /run/secrets/db_password

Environment Variables

# Pass secrets via environment
docker run -d \
  -e DB_PASSWORD=${DB_PASSWORD} \
  -e API_KEY=${API_KEY} \
  my-app

External Secrets Management

HashiCorp Vault

# Integrate with Vault
docker run -d \
  -e VAULT_ADDR=https://vault.example.com \
  -e VAULT_TOKEN=${VAULT_TOKEN} \
  my-app

AWS Secrets Manager

# Use AWS secrets
docker run -d \
  -e AWS_REGION=us-east-1 \
  -e SECRET_ARN=arn:aws:secretsmanager:... \
  my-app

Encrypt Environment Variables

# Use encrypted files
docker run -d \
  --env-file encrypted.env \
  my-app

Resource Limits

Memory Limits

# Set memory limit
docker run -d \
  --memory="1g" \
  --memory-reservation="512m" \
  --memory-swap="2g" \
  --oom-kill-disable=false \
  my-app

CPU Limits

# Limit CPU usage
docker run -d \
  --cpus="2.0" \
  --cpu-shares=1024 \
  --cpuset-cpus="0,1" \
  my-app

Disk I/O Limits

# Limit disk operations
docker run -d \
  --device-read-bps /dev/sda:1mb \
  --device-write-bps /dev/sda:1mb \
  my-app

Process Limits

# Limit number of processes
docker run -d \
  --pids-limit=200 \
  my-app

Security Scanning

Trivy Scanner

# Install Trivy
sudo apt-get install trivy

# Scan image
trivy image my-app:latest

# High and critical only
trivy image --severity HIGH,CRITICAL my-app:latest

# CI/CD integration
trivy image --exit-code 1 --severity CRITICAL my-app:latest

Clair Scanner

# Run Clair
docker run -d --name clair -p 6060:6060 quay.io/coreos/clair:latest

# Scan with clair-scanner
clair-scanner --ip $(hostname -I | awk '{print $1}') my-app:latest

Docker Scan

# Use Docker's built-in scanner
docker scan my-app:latest

# Scan and show base image recommendations
docker scan --file Dockerfile my-app:latest

Anchore

# Scan with Anchore
anchore-cli image add my-app:latest
anchore-cli image wait my-app:latest
anchore-cli image vuln my-app:latest all

Continuous Scanning

# GitHub Actions example
name: Security Scan
on: [push]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build image
        run: docker build -t my-app .
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app'
          format: 'sarif'
          output: 'trivy-results.sarif'

Access Control

Docker Socket Protection

# Never expose Docker socket
# Bad:
# docker run -v /var/run/docker.sock:/var/run/docker.sock ...

# Use Docker-in-Docker or alternative approaches
docker run --privileged docker:dind

User Namespaces

{
  "userns-remap": "default"
}
# Restart Docker
sudo systemctl restart docker

Role-Based Access Control

# Use Docker Enterprise for RBAC
# Or implement custom authorization plugin

# Example: limit users to specific operations
{
  "authorization-plugins": ["docker-authz-plugin"]
}

Audit Docker Commands

# Enable audit logging
sudo auditctl -w /usr/bin/docker -k docker
sudo auditctl -w /var/lib/docker -k docker
sudo auditctl -w /etc/docker -k docker

# View audit logs
sudo ausearch -k docker

Daemon Security

Secure Docker Daemon Configuration

{
  "icc": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true,
  "userland-proxy": false,
  "no-new-privileges": true,
  "seccomp-profile": "/etc/docker/seccomp-profile.json"
}

Enable TLS

{
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/ca.pem",
  "tlscert": "/etc/docker/server-cert.pem",
  "tlskey": "/etc/docker/server-key.pem"
}

Restrict Daemon Access

# Limit who can run Docker commands
sudo usermod -aG docker limited-user

# Better: use sudo for Docker commands
# Don't add users to docker group in production

Disable Legacy Features

{
  "disable-legacy-registry": true
}

Compliance and Auditing

CIS Docker Benchmark

# Run Docker Bench Security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh

Docker Security Scanning

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Verify image signatures
docker pull my-registry.com/image:tag

Log Everything

{
  "log-driver": "syslog",
  "log-opts": {
    "syslog-address": "tcp://siem.example.com:514",
    "tag": "docker/{{.Name}}"
  }
}

Regular Audits

# Check running containers
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}"

# Review images
docker images

# Check volumes
docker volume ls

# Review networks
docker network ls

Production Checklist

Image Security

  • Use official base images
  • Pin specific versions with SHA256
  • Scan for vulnerabilities
  • No secrets in images
  • Sign images with Docker Content Trust
  • Minimal base images (Alpine/distroless)
  • Multi-stage builds
  • Regular image updates

Runtime Security

  • Run as non-root user
  • Read-only root filesystem
  • Drop all capabilities, add only necessary ones
  • Enable no-new-privileges
  • Use security profiles (AppArmor/SELinux/Seccomp)
  • Resource limits (CPU, memory, PIDs)
  • Health checks configured
  • Proper restart policies

Network Security

  • Network isolation per application
  • No unnecessary port exposure
  • Encrypted overlay networks
  • TLS for external communication
  • Network policies implemented

Secrets Management

  • No hardcoded secrets
  • Use Docker secrets or external vault
  • Encrypt secrets at rest and in transit
  • Rotate secrets regularly
  • Audit secret access

Access Control

  • Docker socket never exposed
  • User namespaces enabled
  • RBAC implemented
  • Audit logging enabled
  • Principle of least privilege

Monitoring & Compliance

  • Security scanning in CI/CD
  • Runtime security monitoring
  • Log aggregation configured
  • CIS benchmark compliance
  • Regular security audits
  • Incident response plan

Conclusion

Docker security requires continuous attention across the entire container lifecycle. Implementing these best practices significantly reduces security risks in production environments.

Key Takeaways

  • Defense in Depth: Multiple security layers
  • Least Privilege: Minimum necessary permissions
  • Immutability: Read-only root filesystems
  • Scanning: Continuous vulnerability assessment
  • Secrets: Never in images, always encrypted
  • Monitoring: Audit and log everything

Security Hierarchy

  1. Secure Base Images: Start with trusted, minimal images
  2. Build Security: Scan and sign images
  3. Runtime Restrictions: Capabilities, read-only, non-root
  4. Network Isolation: Separate networks, encrypted communication
  5. Access Control: Protect Docker socket, user namespaces
  6. Monitoring: Continuous security scanning and auditing

Quick Security Commands

# Secure container run
docker run -d \
  --read-only \
  --tmpfs /tmp \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --memory="512m" \
  --cpus="1" \
  --pids-limit=100 \
  --user 1000:1000 \
  my-app

# Scan image
trivy image --severity HIGH,CRITICAL my-app:latest

# Security audit
docker run -it --rm --pid host --userns host --cap-add audit_control \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  --label docker_bench_security \
  docker/docker-bench-security

Next Steps

  1. Audit: Run CIS Docker Benchmark
  2. Scan: Implement continuous security scanning
  3. Harden: Apply runtime security restrictions
  4. Monitor: Set up security monitoring and alerting
  5. Train: Educate team on security best practices
  6. Test: Perform penetration testing
  7. Update: Keep Docker and images updated

Security is an ongoing process. Regularly review and update your security practices to protect against evolving threats.