######################## # Networks & Volumes ######################## networks: traefik_proxy: name: traefik_proxy monitoring: name: monitoring internal: name: internal volumes: traefik_letsencrypt: traefik_logs: portainer_data: umami_db_data: authelia_data: crowdsec_data: prometheus_data: grafana_data: uptime_kuma_data: ######################## # Services ######################## services: ## ───────────────────────────────────────────── ## Traefik — edge router + ACME (HTTP-01) + CrowdSec plugin ## ───────────────────────────────────────────── traefik: image: traefik:v3.1 container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" networks: [traefik_proxy, monitoring] environment: TZ: "${TZ}" command: # Providers - --providers.docker=true - --providers.docker.exposedbydefault=false # Entrypoints - --entrypoints.web.address=:80 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.websecure.address=:443 - --entrypoints.web.forwardedheaders.insecure=true - --entrypoints.websecure.forwardedheaders.insecure=true # Dashboard/API (internal) - --api.dashboard=true # ACME via HTTP-01 (no registrar API needed) - --certificatesresolvers.le.acme.email=${ACME_EMAIL} - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.le.acme.httpchallenge=true - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web # (Alt) Use TLS-ALPN-01 if port 80 is blocked: # - --certificatesresolvers.le.acme.tlschallenge=true # Metrics (Prometheus) - --metrics.prometheus=true - --metrics.prometheus.addrouterslabels=true # Logs - --accesslog.filepath=/var/log/traefik/access.log - --accesslog.bufferingsize=100 - --log.level=INFO # CrowdSec Traefik plugin (recommended vs sidecar) - --experimental.plugins.crowdsecbouncer.moduleName=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin - --experimental.plugins.crowdsecbouncer.version=v1.4.4 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik_letsencrypt:/letsencrypt - traefik_logs:/var/log/traefik labels: - traefik.enable=true # Reusable security headers - traefik.http.middlewares.security-headers.headers.stsSeconds=31536000 - traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true - traefik.http.middlewares.security-headers.headers.stsPreload=true - traefik.http.middlewares.security-headers.headers.browserXssFilter=true - traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true - traefik.http.middlewares.security-headers.headers.referrerPolicy=no-referrer-when-downgrade - traefik.http.middlewares.security-headers.headers.frameDeny=true # Authelia ForwardAuth (reusable) - traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.gate.${DOMAIN} - traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true - traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email # CrowdSec plugin middleware (reusable) - traefik.http.middlewares.crowdsec-plugin.plugin.crowdsecbouncer.enabled=true - traefik.http.middlewares.crowdsec-plugin.plugin.crowdsecbouncer.crowdseclapiurl=http://crowdsec:8080/ - traefik.http.middlewares.crowdsec-plugin.plugin.crowdsecbouncer.crowdseclapikey=${CROWDSEC_BOUNCER_KEY} - traefik.http.middlewares.crowdsec-plugin.plugin.crowdsecbouncer.crowdsecmode=stream # Traefik dashboard (protected) - traefik.http.routers.traefik.rule=Host(`traefik.gate.${DOMAIN}`) - traefik.http.routers.traefik.entrypoints=websecure - traefik.http.routers.traefik.tls.certresolver=le - traefik.http.routers.traefik.service=api@internal - traefik.http.routers.traefik.middlewares=crowdsec-plugin,security-headers ## ───────────────────────────────────────────── ## Portainer — Docker control plane ## ───────────────────────────────────────────── portainer: image: portainer/portainer-ce:latest container_name: portainer restart: unless-stopped networks: [traefik_proxy] volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data labels: - traefik.enable=true - traefik.http.routers.portainer.rule=Host(`portainer.gate.${DOMAIN}`) - traefik.http.routers.portainer.entrypoints=websecure - traefik.http.routers.portainer.tls.certresolver=le - traefik.http.routers.portainer.middlewares=crowdsec-plugin,authelia,security-headers - traefik.http.services.portainer.loadbalancer.server.port=9000 ## ───────────────────────────────────────────── ## Umami + PostgreSQL — privacy analytics ## ───────────────────────────────────────────── umami-db: image: postgres:16 container_name: umami-db restart: unless-stopped environment: POSTGRES_USER: ${UMAMI_DB_USER} POSTGRES_PASSWORD: ${UMAMI_DB_PASS} POSTGRES_DB: ${UMAMI_DB_NAME} TZ: "${TZ}" volumes: - umami_db_data:/var/lib/postgresql/data networks: [internal] umami: image: ghcr.io/umami-software/umami:postgresql-latest container_name: umami restart: unless-stopped depends_on: [umami-db] environment: DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASS}@umami-db:5432/${UMAMI_DB_NAME} APP_SECRET: ${UMAMI_APP_SECRET} TRACKER_SCRIPT_NAME: umami TZ: "${TZ}" networks: [traefik_proxy, internal] labels: - traefik.enable=true - traefik.http.routers.umami.rule=Host(`umami.gate.${DOMAIN}`) - traefik.http.routers.umami.entrypoints=websecure - traefik.http.routers.umami.tls.certresolver=le - traefik.http.routers.umami.middlewares=crowdsec-plugin,authelia,security-headers - traefik.http.services.umami.loadbalancer.server.port=3000 ## ───────────────────────────────────────────── ## Authelia + Redis — SSO/MFA ## ───────────────────────────────────────────── authelia: image: authelia/authelia:latest container_name: authelia restart: unless-stopped depends_on: [redis] environment: TZ: "${TZ}" volumes: - ./authelia/configuration.yml:/config/configuration.yml:ro - ./authelia/users_database.yml:/config/users_database.yml:ro - authelia_data:/config networks: [traefik_proxy, internal] labels: - traefik.enable=true - traefik.http.routers.authelia.rule=Host(`auth.gate.${DOMAIN}`) - traefik.http.routers.authelia.entrypoints=websecure - traefik.http.routers.authelia.tls.certresolver=le - traefik.http.routers.authelia.middlewares=security-headers - traefik.http.routers.authelia.service=authelia-svc - traefik.http.services.authelia-svc.loadbalancer.server.port=9091 redis: image: redis:7-alpine container_name: authelia-redis restart: unless-stopped networks: [internal] ## ───────────────────────────────────────────── ## CrowdSec (LAPI) — with Traefik plugin ## ───────────────────────────────────────────── crowdsec: image: crowdsecurity/crowdsec:latest container_name: crowdsec restart: unless-stopped environment: TZ: "${TZ}" GID: "0" COLLECTIONS: "crowdsecurity/traefik crowdsecurity/linux" volumes: - crowdsec_data:/var/lib/crowdsec/data - ./crowdsec/local_api_server.yaml:/etc/crowdsec/local_api_server.yaml:ro - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro - traefik_logs:/var/log/traefik:ro networks: [traefik_proxy] # Auto-register the API key used by the Traefik plugin crowdsec-init: image: crowdsecurity/crowdsec:latest container_name: crowdsec-init depends_on: [crowdsec] entrypoint: sh -c "cscli bouncers add traefik-bouncer -k ${CROWDSEC_BOUNCER_KEY} || true && sleep 2" networks: [traefik_proxy] restart: "no" ## ───────────────────────────────────────────── ## Uptime Kuma — status page / checks ## ───────────────────────────────────────────── uptime-kuma: image: louislam/uptime-kuma:1 container_name: uptime-kuma restart: unless-stopped volumes: - uptime_kuma_data:/app/data networks: [traefik_proxy] labels: - traefik.enable=true - traefik.http.routers.kuma.rule=Host(`status.gate.${DOMAIN}`) - traefik.http.routers.kuma.entrypoints=websecure - traefik.http.routers.kuma.tls.certresolver=le - traefik.http.routers.kuma.middlewares=crowdsec-plugin,authelia,security-headers - traefik.http.services.kuma.loadbalancer.server.port=3001 ## ───────────────────────────────────────────── ## Prometheus + exporters + Grafana ## ───────────────────────────────────────────── prometheus: image: prom/prometheus:latest container_name: prometheus restart: unless-stopped networks: [monitoring, traefik_proxy] volumes: - prometheus_data:/prometheus - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro labels: - traefik.enable=true - traefik.http.routers.prom.rule=Host(`prometheus.gate.${DOMAIN}`) - traefik.http.routers.prom.entrypoints=websecure - traefik.http.routers.prom.tls.certresolver=le - traefik.http.routers.prom.middlewares=crowdsec-plugin,authelia,security-headers - traefik.http.services.prom.loadbalancer.server.port=9090 cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: cadvisor restart: unless-stopped networks: [monitoring] devices: - /dev/kmsg:/dev/kmsg volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro node-exporter: image: prom/node-exporter:latest container_name: node-exporter restart: unless-stopped networks: [monitoring] pid: host volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: ["--path.rootfs=/rootfs"] grafana: image: grafana/grafana-oss:latest container_name: grafana restart: unless-stopped networks: [traefik_proxy, monitoring] environment: GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}" GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASS}" GF_SERVER_ROOT_URL: https://grafana.gate.${DOMAIN} TZ: "${TZ}" volumes: - grafana_data:/var/lib/grafana labels: - traefik.enable=true - traefik.http.routers.grafana.rule=Host(`grafana.gate.${DOMAIN}`) - traefik.http.routers.grafana.entrypoints=websecure - traefik.http.routers.grafana.tls.certresolver=le - traefik.http.routers.grafana.middlewares=crowdsec-plugin,authelia,security-headers - traefik.http.services.grafana.loadbalancer.server.port=3000