######################## # Networks & Volumes ######################## networks: traefik_proxy: name: traefik_proxy monitoring: name: monitoring internal: name: internal volumes: traefik_letsencrypt: traefik_logs: portainer_data: umami_db_data: prometheus_data: grafana_data: uptime_kuma_data: ######################## # Services ######################## services: ## ───────────────────────────────────────────── ## Traefik — edge router + ACME (HTTP-01) ## ───────────────────────────────────────────── 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 # Experimental plugins - --experimental.plugins.traefik-umami-plugin.modulename=github.com/1cedsoda/traefik-umami-plugin - --experimental.plugins.traefik-umami-plugin.version=v1.0.3 # 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 # Global timeouts for slow backends - --serversTransport.forwardingTimeouts.dialTimeout=30s - --serversTransport.forwardingTimeouts.responseHeaderTimeout=60s - --serversTransport.forwardingTimeouts.idleConnTimeout=180s # Logs - --accesslog.filepath=/var/log/traefik/access.log - --accesslog.bufferingsize=100 - --log.level=INFO 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 # Umami Analytics Plugin Middleware - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.autoTrack=true - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.cache=false - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.doNotTrack=false - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.domains= - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.evadeGoogleTagManager=false - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.forwardPath=umami - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.scriptInjection=true - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.scriptInjectionMode=tag - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.serverSideTracking=false - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.serverSideTrackingMode=all - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.umamiHost=https://analytics.gate.${DOMAIN} - traefik.http.middlewares.umami-analytics.plugin.traefik-umami-plugin.websiteId=${UMAMI_WEBSITE_ID} # 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=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=security-headers,umami-analytics - traefik.http.services.portainer.loadbalancer.server.port=9000 ## ───────────────────────────────────────────── ## Umami + PostgreSQL — analytics backend ## ───────────────────────────────────────────── umami: image: umamisoftware/umami:postgresql-latest container_name: umami depends_on: - umami-db environment: DATABASE_URL: postgresql://umami:${UMAMI_DB_PASS}@umami-db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: ${UMAMI_APP_SECRET} init: true restart: always networks: [traefik_proxy, internal] labels: - traefik.enable=true - traefik.http.routers.umami.rule=Host(`analytics.gate.${DOMAIN}`) - traefik.http.routers.umami.entrypoints=websecure - traefik.http.routers.umami.tls.certresolver=le - traefik.http.routers.umami.middlewares=security-headers - traefik.http.services.umami.loadbalancer.server.port=3000 healthcheck: test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] interval: 5s timeout: 5s retries: 5 umami-db: image: postgres:16-alpine container_name: umami-db restart: always environment: POSTGRES_DB: umami POSTGRES_USER: umami POSTGRES_PASSWORD: ${UMAMI_DB_PASS} TZ: "${TZ}" networks: [internal] volumes: - umami_db_data:/var/lib/postgresql/data ## ───────────────────────────────────────────── ## 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(`uptime.gate.${DOMAIN}`) - traefik.http.routers.kuma.entrypoints=websecure - traefik.http.routers.kuma.tls.certresolver=le - traefik.http.routers.kuma.middlewares=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=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=security-headers - traefik.http.services.grafana.loadbalancer.server.port=3000