Docker Security Best Practices

Docker Security Best Practices

Docker containers provide isolated environments for applications, but security should be a primary concern when building and deploying containers. This guide covers essential security practices to keep your Docker deployments safe and secure.

Understanding Docker Security

Docker security involves multiple layers: the host system, the Docker daemon, containers, and the applications running inside them. Each layer requires proper security configuration.

1. Use Minimal Base Images

Small Attack Surface

Choose minimal base images to reduce the potential attack surface:

# Good: Use Alpine Linux
FROM node:18-alpine

# Better: Use distroless images
FROM gcr.io/distroless/nodejs18-debian11

# Best: Use scratch for compiled applications
FROM scratch

Official vs Community Images

# Prefer official images
FROM nginx:alpine
FROM postgres:15-alpine
FROM redis:7-alpine

# Avoid unverified community images
# FROM randomuser/nginx:latest  # ❌ Avoid this

2. Run as Non-Root User

Create Dedicated User

FROM node:18-alpine

# Create non-root user and group
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

WORKDIR /app

# Copy files and set ownership
COPY --chown=nextjs:nodejs . .

USER nextjs

EXPOSE 3000
CMD ["node", "server.js"]

Multi-Stage with User

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Create user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy with correct ownership
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./package.json

USER nodejs
CMD ["node", "dist/index.js"]

3. Secrets Management

Never Hardcode Secrets

# ❌ BAD: Hardcoded secrets
ENV DATABASE_PASSWORD "mypassword123"
ENV API_KEY "sk-1234567890abcdef"

# ✅ GOOD: Use environment files or Docker secrets
COPY .env.example .env
# Load .env file with actual secrets at runtime

Using Docker Secrets

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    secrets:
      - db_password
      - api_key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true

Environment Variables in Dockerfile

# Use ARG for build-time variables
ARG BUILD_DATE
ARG VCS_REF

# Use ENV for runtime variables
ENV NODE_ENV=production
ENV PORT=3000

# Default values can be overridden
ENV APP_VERSION=${VCS_REF:-unknown}

4. File System Security

Read-Only File System

FROM node:18-alpine

# Create app directory
RUN mkdir -p /app && chown -R 1001:1001 /app

WORKDIR /app

# Copy application
COPY --chown=1001:1001 . .

# Create non-root user
RUN adduser -D -s /bin/sh -u 1001 appuser

USER appuser

# Use read-only root filesystem
EXPOSE 3000
CMD ["node", "server.js"]

Docker Compose with Read-Only Root

version: '3.8'

services:
  app:
    build: .
    read_only: true
    tmpfs:
      - /tmp
      - /run
    volumes:
      - ./logs:/app/logs:rw

Proper File Permissions

FROM python:3.11-slim

# Create directories with proper permissions
RUN mkdir -p /app/logs /app/uploads && \
    groupadd -r appgroup && \
    useradd -r -g appgroup appuser && \
    chown -R appuser:appgroup /app

WORKDIR /app

COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appgroup . .

USER appuser
CMD ["python", "app.py"]

5. Network Security

Expose Only Necessary Ports

FROM nginx:alpine

# Only expose required ports
EXPOSE 80
# Don't expose port 443 if not using HTTPS

Docker Compose Network Isolation

version: '3.8'

services:
  web:
    build: ./web
    networks:
      - frontend
      - backend
    expose:
      - "3000"

  database:
    image: postgres:15-alpine
    networks:
      - backend
    # No expose - only accessible via backend network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    networks:
      - frontend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

6. Resource Limits

Set Resource Constraints

version: '3.8'

services:
  app:
    build: .
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

Docker Run with Limits

# Limit CPU usage
docker run --cpus="0.5" myapp

# Limit memory usage
docker run --memory="512m" myapp

# Set swap limit
docker run --memory="512m" --memory-swap="1g" myapp

# Limit disk I/O
docker run --device-read-bps=/dev/sda:1mb myapp
docker run --device-write-bps=/dev/sda:1mb myapp

7. Health Checks

Implement Health Checks

FROM node:18-alpine

# Install curl for health checks
RUN apk add --no-cache curl

WORKDIR /app
COPY . .
RUN npm ci --only=production

# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "server.js"]

Custom Health Check Script

FROM python:3.11-slim

WORKDIR /app

# Create health check script
RUN echo '#!/bin/bash\n\
curl -f http://localhost:8000/health || exit 1' > /healthcheck.sh && \
chmod +x /healthcheck.sh

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD ["/healthcheck.sh"]

EXPOSE 8000
CMD ["python", "app.py"]

8. Image Scanning and Updates

Using Docker Scout

# Scan image for vulnerabilities
docker scout cves myapp:latest

# Scan during build
docker build --provenance=true --sbom=true -t myapp:latest .

# View detailed scan results
docker scout quickview myapp:latest

Trivy Security Scanner

# Install Trivy
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy

# Scan image
trivy image myapp:latest

# Scan in CI/CD
trivy image --exit-code 0 --severity HIGH,CRITICAL myapp:latest

Automated Security Updates

# .github/workflows/security.yml
name: Security Scan

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'myapp:latest'
        format: 'sarif'
        output: 'trivy-results.sarif'
    
    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

9. Runtime Security

AppArmor/SELinux Profiles

# Enable AppArmor for Docker
sudo systemctl enable apparmor
sudo systemctl start apparmor

# Load default Docker profile
sudo apparmor_parser -r /etc/apparmor.d/docker-default

# Use AppArmor in docker-compose
version: '3.8'

services:
  app:
    build: .
    security_opt:
      - apparmor:docker-default

Seccomp Security Profiles

# Use default seccomp profile in Docker daemon
# Or specify custom profile

# In docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    security_opt:
      - seccomp:./seccomp-profile.json

No New Privileges

version: '3.8'

services:
  app:
    build: .
    security_opt:
      - no-new-privileges:true
    user: "1001:1001"

10. Docker Daemon Security

Secure Docker Daemon

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

Use TLS for Remote Docker

# Generate TLS certificates
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=tcp://hostname:2376 version

# Enable TLS in daemon.json
{
  "tls": true,
  "tlscacert": "/etc/docker/certs.d/ca.pem",
  "tlscert": "/etc/docker/certs.d/server-cert.pem",
  "tlskey": "/etc/docker/certs.d/server-key.pem",
  "tlsverify": true
}

11. Container Runtime Security

Drop Capabilities

version: '3.8'

services:
  app:
    build: .
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID

Remove SUID/SGID Binaries

FROM alpine:latest

# Remove SUID/SGID binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true

# Install only necessary packages
RUN apk add --no-cache \
    ca-certificates \
    && rm -rf /var/cache/apk/*

12. Logging and Monitoring

Centralized Logging

version: '3.8'

services:
  app:
    build: .
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
        labels: "environment,service"

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"

  logstash:
    image: docker.elastic.co/logstash/logstash:8.5.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf

  kibana:
    image: docker.elastic.co/kibana/kibana:8.5.0
    ports:
      - "5601:5601"

Audit Docker Events

# Monitor Docker events
docker events --filter event=start

# Log Docker events to file
docker events --format '{{.Time}} {{.Status}} {{.Actor.Attributes.name}}' >> docker-events.log &

# Monitor resource usage
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"

13. .dockerignore File

Exclude Sensitive Files

# Version control
.git
.gitignore

# Environment files
.env
.env.local
.env.production

# Secrets and keys
*.pem
*.key
*.p12
secrets/

# Development files
node_modules
.vscode
.idea

# Build artifacts
dist
build
coverage

# Logs
*.log
logs/

# OS files
.DS_Store
Thumbs.db

# Temporary files
*.tmp
*.temp
.cache

Security Checklist

Build Phase

  • Use minimal base images
  • Create non-root user
  • Don’t include secrets in images
  • Set proper file permissions
  • Use .dockerignore
  • Sign images with Docker Content Trust

Runtime Phase

  • Use read-only filesystems
  • Set resource limits
  • Use custom networks
  • Drop unnecessary capabilities
  • Enable health checks
  • Use security profiles

Monitoring Phase

  • Scan images regularly
  • Monitor running containers
  • Log security events
  • Update base images
  • Audit Docker daemon
  • Review access controls

Tools and Resources

Security Tools

Security Standards


External Resources:

Related Tutorials:

Last updated on