Docker Security Best Practices: Protecting Containerized Applications

Secure your Docker containers with essential security practices. Learn about image scanning, container isolation, secrets management, and runtime protection.

Docker security shield with container protection icons

Introduction

Docker containers have revolutionized application deployment, but they also introduce security considerations that differ from traditional server deployments. Proper Docker security protects your applications from common threats while maintaining the benefits containerization provides.

This guide covers practical security measures you can implement immediately. The recommendations target real attack vectors and include implementation steps. We focus on measures with meaningful security returns without creating operational burdens that lead administrators to disable protections.

Container security spans three layers: images, runtimes, and infrastructure. We will cover all three layers, creating defense in depth that protects your applications even if one layer fails.

Image Security

Container images form the foundation of your deployments. Securing images prevents vulnerable code from reaching production and reduces your attack surface.

Base Image Selection

Choose minimal base images that contain only necessary components. Smaller images have fewer packages with potential vulnerabilities:

# Use Alpine or distroless images for production
FROM node:20-alpine AS builder

# Or for production, use distroless
FROM gcr.io/distroless/nodejs20-debian12 AS production

Alpine Linux images are significantly smaller than full distributions, reducing both storage requirements and potential vulnerability surfaces. For applications where security is paramount, Google’s distroless images provide even smaller attack surfaces by omitting shells and package managers.

Pin image versions to specific tags rather than using latest. This ensures reproducible builds and prevents unexpected changes:

# Pin to specific version
FROM nginx:1.25-alpine

# Avoid this pattern in production
# FROM nginx:latest

Image Scanning

Automated vulnerability scanning catches security issues before they reach production. Integrate scanning into your build pipeline:

# Scan local image with Trivy
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest \
  image nginx:1.25-alpine

# Scan during CI/CD with GitHub Actions
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'nginx:1.25-alpine'
    format: 'table'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

Configure your scanner to fail builds on critical vulnerabilities. This prevents insecure images from reaching your registry:

# GitHub Actions workflow excerpt
- name: Vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myregistry/myapp:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

Build Secrets

Never bake secrets into images. Build arguments and multi-stage builds protect sensitive information:

# Build stage with access to secrets
FROM node:20-alpine AS builder

# Copy only package files first (caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Build production assets
RUN npm run build

# Production stage without build secrets
FROM node:20-alpine AS production

COPY --from=builder /app/dist /app
COPY --from=builder /app/node_modules /app/node_modules

CMD ["node", "app/index.js"]

This pattern ensures build-time secrets never appear in the final image. Dependencies install in a separate stage, and only production artifacts copy to the final image.

Container Runtime Security

Runtime security protects running containers from exploitation. Even if images contain vulnerabilities, proper runtime configuration limits the impact of potential exploits.

User Permissions

Containers run as root by default, which presents security risks. Create dedicated users for your applications:

# Create application user
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# Switch to unprivileged user
USER appuser

Build images that include application users. Run containers with the --user flag when possible:

# Run container as specific user
docker run --user 1000:1000 myapp:latest

Capability Restrictions

Linux capabilities grant specific privileges beyond what normal users have. Limit capabilities to only those required:

# Run container with minimal capabilities
docker run \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  myapp:latest

The --cap-drop ALL removes all capabilities, and --cap-add adds only necessary ones. Your application likely needs very few additional capabilities beyond basic operation.

Read-Only Filesystems

Prevent containers from modifying their filesystem by mounting it read-only:

# Run container with read-only filesystem
docker run --read-only myapp:latest

For applications that require temporary file storage, use tmpfs mounts:

docker run \
  --read-only \
  --tmpfs /tmp:rw,size=100m,mode=1777 \
  myapp:latest

Resource Limits

Prevent resource exhaustion from affecting other containers or the host:

# Limit CPU and memory
docker run \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1.0 \
  myapp:latest

Memory limits prevent single containers from consuming all host memory. CPU limits prevent compute-intensive containers from affecting other workloads.

Docker Compose Security

Compose configurations should include security settings for production deployments:

version: '3.8'

services:
  app:
    image: myapp:latest
    # Security settings
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp
    
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    
    # Logging with limits
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"
    
    # Networks isolate services
    networks:
      - internal
      
    # Secrets for sensitive data
    secrets:
      - db_password
      - api_key

networks:
  internal:
    driver: bridge
    internal: true

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Secrets Management

Never store secrets in environment variables or image layers. Use dedicated secrets management solutions.

Docker Secrets

Docker Swarm includes built-in secrets management. For single-node or development use, files provide a simple option:

# Create secret from file
echo "my-secret-password" | docker secret create db_password -

# Use secret in Compose
services:
  db:
    image: postgres:15-alpine
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

External Secrets Management

For production deployments, dedicated secrets management tools provide additional features:

# Using HashiCorp Vault with external-secrets
apiVersion: v1
kind: Secret
metadata:
  name: database-credentials
type: Opaque
stringData:
  # References Vault secret
  username:.secretRef: database-username
  password:secretRef: database-password

Tools like HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault integrate with Kubernetes and Docker to provide dynamic secret rotation and audit logging.

Network Security

Container networking requires attention to isolate traffic and prevent unauthorized access.

Network Segmentation

Create separate networks for different application tiers:

# Create custom networks
docker network create --driver bridge frontend
docker network create --driver bridge backend

# Run containers on appropriate networks
docker run \
  --network frontend \
  --network-alias web \
  nginx:latest

docker run \
  --network backend \
  --network-alias database \
  postgres:latest

Containers on the frontend network cannot directly access containers on the backend network without explicit connection. Your application code manages the connection between tiers.

Firewall Integration

Integrate Docker with host firewalls to control external access:

# Allow only specific ports
docker run -p 127.0.0.1:8080:80 nginx:latest

# The 127.0.0.1 binding ensures the port is only accessible locally

Bind to specific IP addresses to restrict which network interfaces accept connections. This prevents accidentally exposing services that should remain internal.

Monitoring and Auditing

Security requires ongoing monitoring to detect and respond to threats.

Container Monitoring

Monitor container behavior for anomalies:

# View container resource usage
docker stats

# Enable content trust for image verification
export DOCKER_CONTENT_TRUST=1

Docker Content Trust ensures you pull only signed images from trusted registries. This prevents supply chain attacks that inject malicious images into public registries.

Logging

Configure comprehensive logging to support incident investigation:

# View container logs
docker logs -f mycontainer

# Configure log drivers for central collection
docker run \
  --log-driver=syslog \
  --log-opt syslog-address=tcp://logserver:514 \
  myapp:latest

Forward logs to a central logging system for analysis and retention. Ensure logs include sufficient detail for forensic investigation while respecting privacy requirements.

Image Signing and Verification

Image signing verifies that images originate from trusted sources and have not been tampered with.

Docker Content Trust

Enable content trust to require image signing:

# Enable content trust globally
export DOCKER_CONTENT_TRUST=1

# Or enable for specific repositories
export DOCKER_CONTENT_TRUST_SERVER=https://notary.example.com
export DOCKER_CONTENT_TRUST_CERT_PATH=/path/to/ca.crt

When enabled, Docker only pulls signed images. Publishers must sign images before distribution, preventing unsigned images from reaching your infrastructure.

Notary

Deploy a Notary server for private registries:

# Docker Compose for Notary
services:
  notary-server:
    image: notary-server:latest
    ports:
      - "4443:4443"
    environment:
      NOTARY_SERVER_STORAGE_TYPE: mysql
      NOTARY_SERVER_TRUST_POOL: ""

Notary provides the infrastructure for signing and verifying images in private registries. Integrate it with your CI/CD pipeline to sign images automatically after successful builds.

Conclusion

Docker security requires attention across multiple layers, from base image selection through runtime configuration to monitoring. The practices in this guide create defense in depth that protects your applications.

Effective container security balances protection with operational requirements. Start with the measures that provide the greatest security benefit with minimal friction. Add additional layers as your threat model requires. Regular security reviews ensure your configurations remain effective as your applications evolve.


Related Posts: