Skip to main content
Docker

Secure Your Docker Stack

How to isolate containers, manage secrets, set up a reverse proxy with TLS, scan images for vulnerabilities, and keep everything patched.

Back to Security Lab

Docker makes self-hosting easy, but convenience comes with risk. A misconfigured container can expose your entire host. This guide covers the security fundamentals every Docker homelab needs — from image hygiene to network isolation to secrets management.

1. Image Hygiene

Every container starts with an image. If the image is compromised or bloated, no amount of runtime security will save you.

  • Use official or verified images — prefer images from Docker Hub verified publishers or the official library. Check the "Docker Official Image" badge.
  • Pin versions — never use :latest in production. Pin to a specific version tag (e.g. nginx:1.25-alpine) so updates are deliberate, not accidental.
  • Prefer Alpine-based images — smaller images have fewer packages and therefore a smaller attack surface. Alpine images are typically 5-10x smaller than their Debian counterparts.
  • Scan images — use docker scout cves <image> or Trivy (trivy image <image>) to scan for known CVEs before deploying.

Scan an image with Trivy

# Install Trivy
sudo apt install trivy

# Scan before you deploy
trivy image nextcloud:28-apache

# Scan your running containers
docker ps --format '{{.Image}}' | xargs -I{} trivy image {}

2. Container Isolation

By default, Docker containers share the host kernel and can escalate privileges. Lock them down.

  • Never run as root — add user: "1000:1000" to your compose services. Most apps work fine as a non-root user.
  • Drop capabilities — containers inherit Linux capabilities they rarely need. Drop all and add back only what is required.
  • Read-only filesystem — set read_only: true and mount writable volumes only where the app needs to write.
  • No new privileges — prevent privilege escalation with security_opt: [no-new-privileges:true].
  • Never mount the Docker socket — unless absolutely necessary (Traefik, Watchtower). A container with socket access controls the entire host.

Hardened compose service example

services:
  app:
    image: myapp:1.2.0
    user: "1000:1000"
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    tmpfs:
      - /tmp
    volumes:
      - ./data:/app/data

3. Docker Network Isolation

By default, all containers on the same Docker bridge can talk to each other. That is a problem.

  • Create dedicated networks — group related services on their own bridge network. Your database should not be on the same network as your public-facing web app.
  • Disable inter-container communication on the default bridge: "com.docker.network.bridge.enable_icc": "false".
  • Never expose ports to 0.0.0.0 — bind to 127.0.0.1 if the service is only used by other containers or your reverse proxy: 127.0.0.1:8080:80.

Network isolation in compose

services:
  web:
    networks: [frontend, backend]
  db:
    networks: [backend]
  proxy:
    networks: [frontend]
    ports:
      - "443:443"

networks:
  frontend:
  backend:
    internal: true  # no internet access

4. Secrets Management

Passwords in plain-text compose files are a breach waiting to happen. Use proper secrets handling.

  • .env files — at minimum, move secrets to a .env file and reference them with ${VAR}. Set file permissions to 600.
  • Docker secrets — for Swarm mode, use docker secret create. Secrets are mounted as files in /run/secrets/ and never stored in the image layer.
  • Never commit secrets — add .env, *.key, and *.pem to your .gitignore. Use git-secrets or gitleaks as a pre-commit hook.
  • Rotate regularly — database passwords, API keys, and TLS certificates should all have a rotation schedule. Automate it where possible.

5. Reverse Proxy & TLS

Never expose services directly. Put everything behind a reverse proxy with automatic TLS.

  • Traefik or Nginx Proxy Manager — both handle Let's Encrypt auto-renewal. Traefik discovers services via Docker labels; NPM has a web UI.
  • Force HTTPS — redirect all HTTP to HTTPS. Set HSTS headers with a minimum 6-month max-age.
  • TLS 1.2 minimum — disable TLS 1.0 and 1.1. Use strong cipher suites.
  • Rate limiting — add rate limits to login endpoints and APIs to slow brute-force attacks.
  • Auth middleware — for internal tools (Portainer, Grafana), add Authelia or Authentik as an SSO layer with 2FA.

Traefik with Let's Encrypt (compose labels)

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.app.rule=Host(`app.yourdomain.com`)"
  - "traefik.http.routers.app.tls.certresolver=letsencrypt"
  - "traefik.http.routers.app.middlewares=secure-headers"
  - "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000"
  - "traefik.http.middlewares.secure-headers.headers.forceSTSHeader=true"

6. Logging & Updates

Containers are ephemeral but their logs are gold. And unpatched containers are the easiest target.

  • Centralise logs — use the Docker logging driver to send logs to Loki, Graylog, or a syslog server. Do not rely on docker logs alone — it is lost when the container is recreated.
  • Watchtower for auto-updates — schedule updates during low-traffic hours: --schedule "0 4 * * 1" (4am every Monday). Enable notifications so you know what was updated.
  • Test before production — run a staging compose stack that updates first. If it survives 24 hours, update production.
  • Prune regularly — dangling images and stopped containers waste disk and can contain old vulnerable versions: docker system prune -a --volumes (use with care).
Next step: Your containers are secure — now lock down your home network with firewall rules, DNS filtering, and intrusion detection.