services: traefik: image: traefik:latest container_name: traefik restart: always security_opt: - no-new-privileges:true networks: - traefik - crowdsec ports: - "80:80" - "443:443" # Port 8080 is deliberately NOT published. The dashboard is reached # through the authenticated router below. Publishing 8080 only exposes # the API when `api.insecure: true`, which we do not set. environment: - TZ=America/Denver # Route 53 DNS-01 challenge. Values live in .env (gitignored). - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_REGION=${AWS_REGION} volumes: - /etc/localtime:/etc/localtime:ro - /var/run/docker.sock:/var/run/docker.sock:ro # Static config differs between hosts ONLY in the ACME challenge type: # prod uses HTTP-01 (public port 80 lands there), dev must use DNS-01. # Selected via .env so both environments share one branch instead of # diverging permanently. - ${TRAEFIK_STATIC_CONFIG:-./traefik.yml}:/traefik.yml:ro - ./acme.json:/acme.json - ./config.yml:/config.yml:ro - ./logs:/var/log/traefik labels: - "traefik.enable=true" # Dashboard — LAN-only AND password protected. - "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)" - "traefik.http.routers.traefik.entrypoints=https" - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" - "traefik.http.routers.traefik.service=api@internal" - "traefik.http.routers.traefik.middlewares=traefik-auth,internal-whitelist@file" # Credentials come from .env (gitignored), not from this file. # Generate with: htpasswd -nb admin 'yourpassword' - "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}" # Named redirect middleware, referenced by site stacks on their HTTP router. - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" # CrowdSec bouncer. # # Defined HERE rather than in config.yml on purpose: Traefik does not # interpolate environment variables in its YAML config files, so the API # key would have to be written literally into config.yml — which is why # that file was previously gitignored and the repo lost its "config lives # in git" property. Compose DOES substitute ${...} in labels, so defining # it here keeps the key in .env alone and lets config.yml be committed. # # Consequence: this middleware is `crowdsec-bouncer@docker`, NOT `@file`. # Site stacks must reference it with the @docker suffix. - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.enabled=true" - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecMode=live" - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiKey=${CROWDSEC_LAPI_KEY}" - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiHost=crowdsec:8080" - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.crowdsecLapiScheme=http" - "traefik.http.middlewares.crowdsec-bouncer.plugin.bouncer.forwardedHeadersCustomName=X-Custom-Header" networks: # Pre-create once per host: docker network create traefik traefik: name: traefik external: true crowdsec: name: crowdsec external: true