Skip to main content

Docker Security Best Practices - Lessons from Production

· 4 min read
Pascal Nehlsen
DevSecOps Engineer

Containers have revolutionized deployment, but they've also introduced new security challenges. Here's what I learned securing Docker in production.

The Wake-Up Call

Last month, a security scan revealed our Docker images had 247 vulnerabilities. Many were critical. This post covers how we got that number down to 3 (all low-severity).

1. Use Minimal Base Images

Bad: Full OS Image

FROM ubuntu:latest
RUN apt-get update && apt-get install -y python3 python3-pip
# Image size: 1.2GB, 100+ vulnerabilities

Good: Minimal Image

FROM python:3.11-slim-bookworm
# Image size: 180MB, 5-10 vulnerabilities

Better: Distroless

# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Runtime stage
FROM gcr.io/distroless/python3-debian11
COPY --from=builder /root/.local /root/.local
COPY . /app
WORKDIR /app
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
# Image size: 120MB, 0-2 vulnerabilities

2. Run as Non-Root User

The Problem

# Running as root (UID 0) - dangerous!
FROM python:3.11-slim
COPY . /app
CMD ["python", "app.py"]

The Solution

FROM python:3.11-slim

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Set up application directory
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

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

3. Scan Images in CI/CD

Using Trivy

# .gitlab-ci.yml
container:scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false

Using Snyk

container:snyk:
stage: security
image: snyk/snyk:docker
script:
- snyk auth $SNYK_TOKEN
- snyk container test $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --severity-threshold=high

4. Multi-Stage Builds

Separate build dependencies from runtime:

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

# Stage 2: Runtime
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app

# Copy only necessary files
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules

USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

5. Secrets Management

Never Do This

# NEVER HARDCODE SECRETS!
ENV DATABASE_PASSWORD=supersecret123
ENV API_KEY=sk_live_abc123def456

Use Docker Secrets (Swarm)

FROM python:3.11-slim
WORKDIR /app
COPY . .

# Read secrets from files mounted by Docker
CMD python -c "import os; \
db_pass = open('/run/secrets/db_password').read().strip(); \
os.environ['DB_PASSWORD'] = db_pass" && python app.py

Use Environment Variables (Kubernetes)

apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password

6. Limit Container Capabilities

# docker-compose.yml
services:
app:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if needed
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /var/run

7. Network Segmentation

# docker-compose.yml
version: '3.8'

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

services:
web:
networks:
- frontend
- backend

database:
networks:
- backend # Only accessible from backend network

8. Resource Limits

Prevent DoS attacks:

services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
ulimits:
nofile:
soft: 1024
hard: 2048

9. Automated Vulnerability Scanning

Daily Scheduled Scans

# .gitlab-ci.yml
schedule:scan:
stage: security
only:
- schedules
script:
- trivy image --format json --output scan-results.json $CI_REGISTRY_IMAGE:latest
- python scripts/notify_security_team.py scan-results.json

10. Image Signing & Verification

Sign Images with Docker Content Trust

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Push signed image
docker push myregistry.io/myapp:latest

# Verify signature
docker pull myregistry.io/myapp:latest

Security Checklist

  • Use minimal base images (Alpine, Distroless)
  • Run containers as non-root user
  • Scan images in CI/CD pipeline
  • Use multi-stage builds
  • Never hardcode secrets
  • Drop unnecessary capabilities
  • Implement network segmentation
  • Set resource limits
  • Enable automated vulnerability scanning
  • Sign and verify images
  • Keep base images updated
  • Use .dockerignore to exclude sensitive files

Real Impact

Before optimization:

  • Image size: 1.2GB
  • Vulnerabilities: 247 (15 critical)
  • Build time: 8 minutes

After optimization:

  • Image size: 145MB (87% reduction)
  • Vulnerabilities: 3 (0 critical)
  • Build time: 3 minutes

Resources


Securing your containers? Share your experience on GitHub!