DanpLab · Lab NoteArchitettura operativa

Docker Compose Hardening: Complete Guide for Secure Homelabs

Technical deep dive on security best practices for Docker Compose in homelab environments. Configurations, privilege restrictions, and isolation to protect containers.

3 min readBased on real operational use
dockercomposesecurityhardeninghomelabcontainers

Docker Compose is an essential tool for managing container stacks in homelab environments, but security is often overlooked. In this guide, we dive deep into hardening techniques to protect your self-hosted services.

Why Harden Docker Compose?

Docker containers, if not configured properly, can pose a security risk to the entire homelab. Elevated privileges, dangerous mount points, and open network configurations are common vulnerabilities.

Fundamental Best Practices

1. User Namespace Remapping

User namespace remapping is one of the most effective techniques to prevent privilege escalation:

services:
  app:
    image: nginx:alpine
    user: "1000:1000"
    security_opt:
      - "no-new-privileges:true"

2. Capability Limitation

Removing unnecessary capabilities drastically reduces the attack surface:

services:
  database:
    image: postgres:15
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID

3. Read-Only Filesystem

Configure containers with read-only filesystems whenever possible:

services:
  web:
    image: nginx:alpine
    read_only: true
    tmpfs:
      - /tmp
      - /run
      - /var/cache/nginx

Secure Network Configuration

Network Isolation

Use dedicated Docker networks for each stack:

networks:
  app-network:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.28.0.0/16

services:
  app:
    networks:
      - app-network

Port Limitation

Expose only strictly necessary ports:

services:
  webapp:
    ports:
      - "8080:80"  # Solo HTTP
      # - "8443:443" # Solo se serve HTTPS

Volume and Mount Point Management

Named Volumes vs Bind Mounts

Prefer named volumes over bind mounts for greater security:

volumes:
  app-data:
    driver: local

services:
  app:
    volumes:
      - app-data:/app/data
      # Evitare: - /host/path:/container/path

Access Restrictions

services:
  database:
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - PGDATA=/var/lib/postgresql/data/pgdata

Resource Limits and Security Opt

Resource Limitation

services:
  app:
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 512M
        reservations:
          cpus: "0.5"
          memory: 256M

Advanced Security Options

services:
  critical-app:
    security_opt:
      - "apparmor:docker-default"
      - "seccomp=unconfined"
    pids_limit: 100

Complete Hardening Checklist

  • [ ] User namespace remapping configured
  • [ ] Unnecessary capabilities removed
  • [ ] Read-only filesystem where possible
  • [ ] Isolated and internal networks
  • [ ] Only necessary ports exposed
  • [ ] Named volumes instead of bind mounts
  • [ ] Appropriate resource limits
  • [ ] Security opt configured
  • [ ] No-new-privileges enabled
  • [ ] PIDs limit set

Complete Hardened docker-compose.yml Example

version: "3.8"

networks:
  app-net:
    driver: bridge
    internal: true
    ipam:
      config:
        - subnet: 172.22.0.0/16

volumes:
  app-data:
    driver: local
  db-data:
    driver: local

services:
  web:
    image: nginx:alpine
    user: "101:101"
    read_only: true
    tmpfs:
      - /tmp
      - /run
      - /var/cache/nginx
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - "no-new-privileges:true"
    networks:
      - app-net
    ports:
      - "8080:80"
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "1"

  app:
    image: node:18-alpine
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - "no-new-privileges:true"
    networks:
      - app-net
    volumes:
      - app-data:/app/data
    environment:
      - NODE_ENV=production
      - PORT=3000

  database:
    image: postgres:15-alpine
    user: "999:999"
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID
    security_opt:
      - "no-new-privileges:true"
    networks:
      - app-net
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD_FILE=/run/s...rd
    secrets:
      - db_password

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

Conclusions

Docker Compose hardening is not a one-time process but a continuous practice. Every new service added to the stack should be evaluated against these guidelines. Container security is an investment that protects not only individual services but the entire homelab infrastructure.

Remember: perfect security doesn't exist, but reducing the attack surface is the first step toward a more resilient homelab.

Sources: OWASP Docker Security Cheat Sheet, Docker Official Documentation, Reddit r/homelab discussions