01Introduction
Self-hosting puts you in control, but also makes you responsible for security. Docker provides isolation, but misconfiguration can expose your services to attack. This guide covers essential security practices for self-hosted Docker deployments.
02Keep Images Updated
Container images include operating systems and libraries with potential vulnerabilities. Regular updates are your first line of defense.
1# Check for updated images2docker compose pull34# Update and recreate containers5docker compose up -d67# Remove old unused images8docker image prune -a910# Automate with Watchtower (use cautiously)11services:12 watchtower:13 image: containrrr/watchtower14 volumes:15 - /var/run/docker.sock:/var/run/docker.sock16 environment:17 - WATCHTOWER_CLEANUP=true18 - WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM dailyWatchtower auto-updates can break things. Consider using it only for monitoring (WATCHTOWER_MONITOR_ONLY=true) and updating manually.
03Network Segmentation
Don't put all containers on the same network. Segment by trust level—public-facing services shouldn't directly access databases.
1services: 2 # Public-facing reverse proxy3 traefik: 4 networks: 5 - frontend6 - backend78 # Application tier9 app: 10 networks: 11 - backend12 - database1314 # Database tier - most restricted15 postgres: 16 networks: 17 - database1819networks: 20 frontend: 21 # Exposed to internet via Traefik22 backend: 23 internal: true # No external access24 database: 25 internal: true # No external accessUse internal: true for networks that should never be accessible from outside Docker.
04Read-Only Containers
Making the container filesystem read-only prevents attackers from modifying binaries or planting malware. Combine with tmpfs for directories that need writes.
1services: 2 app: 3 image: myapp:1.04 read_only: true5 tmpfs: 6 - /tmp7 - /var/run8 volumes: 9 - app_data:/data # Only this is writable1011# Not all images support read-only mode12# Test first: docker run --read-only myapp:1.005Drop Linux Capabilities
By default, containers get many Linux capabilities they don't need. Drop all and add back only what's required.
1services: 2 app: 3 image: myapp:1.04 cap_drop: 5 - ALL6 cap_add: 7 - NET_BIND_SERVICE # Only if binding to ports < 102489 # For web servers, you might need:10 nginx: 11 cap_drop: 12 - ALL13 cap_add: 14 - CHOWN15 - SETGID16 - SETUID17 - NET_BIND_SERVICE06Prevent Privilege Escalation
The no-new-privileges flag prevents processes from gaining additional privileges via setuid binaries or other mechanisms.
1services: 2 app: 3 image: myapp:1.04 security_opt: 5 - no-new-privileges:true67# This prevents attacks like:8# - Exploiting setuid binaries9# - Using sudo inside containers10# - Privilege escalation via kernel exploits07Resource Limits
Limit CPU and memory to prevent denial-of-service attacks and contain misbehaving containers.
1services: 2 app: 3 image: myapp:1.04 deploy: 5 resources: 6 limits: 7 cpus: '1'8 memory: 512M9 pids: 100 # Prevent fork bombs10 reservations: 11 cpus: '0.25'12 memory: 128Mpids limit prevents fork bomb attacks that spawn processes until the system crashes.
08Secrets Management
Never put secrets in environment variables visible in docker inspect. Use Docker secrets or mount secret files.
1services: 2 app: 3 image: myapp:1.04 secrets: 5 - db_password6 - api_key7 environment: 8 # Reference file, not value9 - DB_PASSWORD_FILE=/run/secrets/db_password1011secrets: 12 db_password: 13 file: ./secrets/db_password.txt14 api_key: 15 file: ./secrets/api_key.txt1617# The secret is mounted at /run/secrets/<name>18# Never visible in docker inspect or logs09Protect the Docker Socket
Access to the Docker socket equals root access to the host. Protect it carefully.
1# If you must expose Docker socket, use a proxy2services: 3 # Socket proxy limits which API calls are allowed4 docker-proxy: 5 image: tecnativa/docker-socket-proxy6 environment: 7 - CONTAINERS=18 - IMAGES=19 - INFO=110 - NETWORKS=011 - VOLUMES=012 - POST=0 # Read-only access13 volumes: 14 - /var/run/docker.sock:/var/run/docker.sock:ro15 networks: 16 - docker-api1718 # Services connect to proxy, not socket directly19 traefik: 20 environment: 21 - DOCKER_HOST=tcp://docker-proxy:237522 networks: 23 - docker-api2425networks: 26 docker-api: 27 internal: trueAny container with Docker socket access can take over the entire host. Treat it like root SSH access.
10Implement Healthchecks
Healthchecks detect compromised or malfunctioning containers. Combined with restart policies, they ensure self-healing.
1services: 2 app: 3 image: myapp:1.04 healthcheck: 5 test: ["CMD", "curl", "-f", "http://localhost/health"]6 interval: 30s7 timeout: 10s8 retries: 39 start_period: 40s10 restart: unless-stopped1112 postgres: 13 image: postgres:1514 healthcheck: 15 test: ["CMD-SHELL", "pg_isready -U postgres"]16 interval: 10s17 timeout: 5s18 retries: 511Logging and Monitoring
You can't secure what you can't see. Centralize logs and monitor for anomalies.
1services: 2 app: 3 logging: 4 driver: "json-file"5 options: 6 max-size: "10m"7 max-file: "5"8 labels: "service,environment"9 labels: 10 service: "myapp"11 environment: "production"1213# Consider centralized logging14 loki: 15 image: grafana/loki:latest16 volumes: 17 - loki_data:/loki1819 promtail: 20 image: grafana/promtail:latest21 volumes: 22 - /var/lib/docker/containers:/var/lib/docker/containers:ro23 - /var/run/docker.sock:/var/run/docker.sock12Security Checklist
Run through this checklist for every deployment:
- [ ] Images pinned to specific versions
- [ ] Containers running as non-root
- [ ] Unnecessary capabilities dropped
- [ ] No Docker socket mounted (or proxied)
- [ ] Secrets not in environment variables
- [ ] Networks segmented appropriately
- [ ] Resource limits configured
- [ ] Healthchecks implemented
- [ ] Logging configured with rotation
- [ ] Host firewall configured
- [ ] Automatic updates planned