Docker is powerful, but with great power comes great responsibility. A misconfigured container can compromise your entire host. Security vulnerabilities in images can expose your data. In this lesson, you'll learn the security practices and operational best practices that separate hobby projects from production deployments.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Run containers as non-root users
  • Scan images for vulnerabilities
  • Manage secrets securely
  • Set resource limits on containers
  • Use read-only root filesystems
  • Drop unnecessary Linux capabilities
  • Follow Dockerfile security best practices

2. Why This Matters

Real-world scenario: A vulnerability in your application allows remote code execution. If your container runs as root, the attacker owns your host. If your container has network access to internal services, they can pivot. Security in containers isn't optional—it's essential.

3. Core Concepts

Run as Non-Root User

# ❌ BAD: Running as root
FROM node:18
COPY . .
CMD ["node", "app.js"]

# ✅ GOOD: Create and switch to non-root user
FROM node:18-alpine

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

# Set ownership
COPY --chown=nodejs:nodejs . .

# Switch user
USER nodejs

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

# Python example
FROM python:3.11-slim

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

# Set ownership
COPY --chown=appuser:appuser . .

# Switch user
USER appuser

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

# Verify user in container
docker run myapp whoami
# Output: nodejs (not root!)
Running containers as non-root

Drop Capabilities

# Check current capabilities
docker run --rm nginx capsh --print

# Drop all capabilities, then add only needed ones
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

# Drop specific dangerous capabilities
docker run --cap-drop=SYS_ADMIN --cap-drop=NET_ADMIN nginx

# Docker Compose
# docker-compose.yml:
# services:
#   app:
#     cap_drop:
#       - ALL
#     cap_add:
#       - NET_BIND_SERVICE

# Common capabilities by service type
# Web servers: NET_BIND_SERVICE (bind to port <1024)
# Database: CHOWN, DAC_OVERRIDE, SETUID, SETGID
# Monitoring: NET_RAW (ping), NET_ADMIN (if needed)
Managing Linux capabilities

Resource Limits

# Memory limits
docker run --memory=512m --memory-swap=1g nginx
docker run --memory=256m --memory-reservation=128m nginx

# CPU limits
docker run --cpus=0.5 nginx           # 50% of one CPU
docker run --cpus=2 nginx             # 2 full CPUs
docker run --cpus=0.5 --cpuset-cpus=0 nginx  # Use specific CPU core

# IO limits (Block IO)
docker run --device-write-bps=/dev/sda:10mb nginx

# Docker Compose
# services:
#   app:
#     deploy:
#       resources:
#         limits:
#           cpus: '0.5'
#           memory: 512M
#         reservations:
#           cpus: '0.25'
#           memory: 256M

# Show resource usage
docker stats
docker stats mycontainer
Setting container resource limits

Read-Only Root Filesystem

# Run with read-only root
docker run --read-only nginx

# Add writable volumes for necessary paths
docker run --read-only -v /tmp:/tmp -v /var/log:/var/log nginx

# Dockerfile design for read-only
FROM nginx:alpine

# Create writable directories as volumes
VOLUME ["/tmp", "/var/log/nginx", "/var/cache/nginx"]

# Ensure no writes to container filesystem
# All writes go to volumes

# Docker Compose
# services:
#   app:
#     read_only: true
#     tmpfs:
#       - /tmp
#       - /var/run
Read-only root filesystem for security

Secrets Management

# ❌ BAD: Secrets in environment variables
# docker run -e DATABASE_PASSWORD=secret123 postgres

# ❌ BAD: Secrets in Dockerfile
# ENV DATABASE_PASSWORD=secret123

# ❌ BAD: Secrets in image layers
# COPY .env .

# ✅ GOOD: Docker Secrets (Swarm mode)
echo "mysecretpassword" | docker secret create db_password -
docker service create --secret db_password --name postgres postgres

# ✅ GOOD: BuildKit secrets (build time)
# docker build --secret id=github_token,src=./token.txt .
# Dockerfile:
# RUN --mount=type=secret,id=github_token \
#     GITHUB_TOKEN=$(cat /run/secrets/github_token) pip install --extra-index-url=https://...

# ✅ GOOD: External secrets (Vault, AWS Secrets Manager, etc.)
# docker run -e VAULT_TOKEN=$(vault read -field=token secret/db) app

# ✅ GOOD: .env files (NOT committed to git)
# docker run --env-file .env.production app

# ✅ GOOD: Docker Compose secrets
# secrets:
#   db_password:
#     file: ./secrets/db_password.txt
# services:
#   db:
#     secrets:
#       - db_password
Secrets management best practices

Image Scanning

# Docker Scout (built-in)
docker scout quickview nginx:latest
docker scout cves nginx:latest
docker scout recommendations nginx:latest

# Trivy (open-source)
# Install: brew install aquasecurity/trivy/trivy
trivy image nginx:latest
trivy image --severity CRITICAL python:3.11-slim
trivy image --format json --output report.json myapp

# Grype (another scanner)
grype nginx:latest

# Snyk (commercial)
snyk container test nginx:latest

# GitHub Actions integration
# name: Scan image
# run: |
#   trivy image --severity HIGH,CRITICAL --exit-code 1 myimage:latest

# CI/CD pipeline scanning
# Scan before pushing to registry
# Block builds on critical vulnerabilities
Container image vulnerability scanning

Security Contexts

# Security options
docker run --security-opt=no-new-privileges nginx
docker run --security-opt=seccomp=./seccomp-profile.json nginx

# AppArmor (Ubuntu/Debian)
docker run --security-opt apparmor=my-profile nginx

# SELinux (RHEL/CentOS)
docker run --security-opt label=type:my_container_t nginx

# Disable unnecessary kernel features
# Prevent privilege escalation
docker run --security-opt=no-new-privileges:true nginx

# Set user namespace (userns-remap)
# Map container root to non-privileged host user
docker run --userns=host nginx

# Docker Compose
# services:
#   app:
#     security_opt:
#       - no-new-privileges:true
#       - seccomp:./seccomp.json
Advanced security contexts

4. Complete Project: Secure Production Container

# Dockerfile - Secure production Python app

# Stage 1: Builder
FROM python:3.11-slim AS builder

# Install security updates
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy and install dependencies
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.11-slim

# Install runtime dependencies only
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

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

WORKDIR /app

# Copy from builder
COPY --from=builder --chown=appuser:appuser /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .

# Set environment
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1

# Security: drop capabilities
RUN setcap -r /usr/bin/python3.11 || true

# Switch to non-root user
USER appuser

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

EXPOSE 8000

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
Secure production Dockerfile
# Run securely
docker run -d \
  --name secure-app \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --tmpfs /var/run:rw,noexec,nosuid,size=64m \
  --memory=512m \
  --cpus=0.5 \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --security-opt=seccomp=./seccomp-default.json \
  -e DATABASE_URL="postgresql://user:pass@db/db" \
  --network=backend \
  myapp:latest
Secure container runtime command
# docker-compose.secure.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.secure
    read_only: true
    tmpfs:
      - /tmp:size=64m,noexec,nosuid
      - /var/run:size=64m,noexec,nosuid
    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp-profile.json
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    environment:
      - DATABASE_URL=postgresql://user:pass@db/db
    networks:
      - backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend
    restart: unless-stopped

networks:
  backend:
    internal: true

volumes:
  postgres_data:
Secure Docker Compose configuration

5. Common Security Vulnerabilities

6. Summary Checklist

7. Next Steps

Next category: Kubernetes - Container Orchestration

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 6, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment