######################## # Networks & Volumes ######################## networks: traefik_proxy: name: traefik_proxy volumes: traefik_letsencrypt: traefik_logs: portainer_data: uptime_kuma_data: umami_data: pgadmin_data: authelia_db_data: beszel_data: gitea_data: gitea_db_data: duplicati_config: ######################## # Services ######################## services: ## ───────────────────────────────────────────── ## Traefik — edge router + ACME (HTTP-01) ## ───────────────────────────────────────────── traefik: image: traefik:latest container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" # Mail protocol ports for MailCow integration - "25:25" # SMTP - "465:465" # SMTPS - "587:587" # Submission - "143:143" # IMAP - "993:993" # IMAPS - "110:110" # POP3 - "995:995" # POP3S - "4190:4190" # ManageSieve networks: [traefik_proxy] environment: TZ: "${TZ}" command: # Enable ping endpoint for health checks - --ping=true # Experimental plugins - --experimental.plugins.traefik-umami-plugin.modulename=github.com/1cedsoda/traefik-umami-plugin - --experimental.plugins.traefik-umami-plugin.version=v1.0.3 # 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 # Mail protocol entrypoints for MailCow integration - --entrypoints.smtp.address=:25 - --entrypoints.smtps.address=:465 - --entrypoints.submission.address=:587 - --entrypoints.imap.address=:143 - --entrypoints.imaps.address=:993 - --entrypoints.pop3.address=:110 - --entrypoints.pop3s.address=:995 - --entrypoints.sieve.address=:4190 # Dashboard/API (internal) - --api=true - --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 # 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 - --metrics.prometheus=true volumes: - /var/run/docker.sock:/var/rundocker.sock:ro - traefik_letsencrypt:/letsencrypt - traefik_logs:/var/log/traefik labels: - "traefik.enable=true" # Reusable security headers middleware - "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" # Authelia middleware - "traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/authz/forward-auth" - "traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true" - "traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Email,Remote-Name" # Traefik dashboard (protected) - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN_PREFIX}.${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=authelia@docker,security-headers@docker" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s ## ───────────────────────────────────────────── ## Authelia — authentication and authorization ## ───────────────────────────────────────────── authelia: image: authelia/authelia:latest container_name: authelia restart: unless-stopped networks: [traefik_proxy] volumes: - ./authelia:/config entrypoint: /bin/sh command: - -c - | eval "echo \"$(cat /config/configuration.template.yml)\"" > /config/configuration.yml exec /app/entrypoint.sh environment: TZ: "${TZ}" DOMAIN: "${DOMAIN}" DOMAIN_PREFIX: "${DOMAIN_PREFIX}" AUTHELIA_DB_NAME: "${AUTHELIA_DB_NAME}" AUTHELIA_DB_USER: "${AUTHELIA_DB_USER}" AUTHELIA_DB_PASSWORD: "${AUTHELIA_DB_PASSWORD}" AUTHELIA_SESSION_SECRET: '${AUTHELIA_SESSION_SECRET}' AUTHELIA_STORAGE_ENCRYPTION_KEY: '${AUTHELIA_STORAGE_ENCRYPTION_KEY}' AUTHELIA_STORAGE_POSTGRES_PASSWORD: '${AUTHELIA_DB_PASSWORD}' AUTHELIA_NOTIFIER_SMTP_PASSWORD: '${AUTHELIA_NOTIFIER_SMTP_PASSWORD}' AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: '${AUTHELIA_JWT_SECRET}' depends_on: - authelia-db labels: - "traefik.enable=true" - "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.authelia.entrypoints=websecure" - "traefik.http.routers.authelia.tls.certresolver=le" - "traefik.http.routers.authelia.middlewares=security-headers@docker" - "traefik.http.services.authelia.loadbalancer.server.port=9091" ## ───────────────────────────────────────────── ## Authelia Database — PostgreSQL ## ───────────────────────────────────────────── authelia-db: image: postgres:15-alpine container_name: authelia-db restart: unless-stopped networks: [traefik_proxy] environment: POSTGRES_DB: ${AUTHELIA_DB_NAME} POSTGRES_USER: ${AUTHELIA_DB_USER} POSTGRES_PASSWORD: ${AUTHELIA_DB_PASSWORD} volumes: - authelia_db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"] interval: 30s timeout: 10s retries: 3 start_period: 30s ## ───────────────────────────────────────────── ## Portainer — Docker control plane ## ───────────────────────────────────────────── portainer: image: portainer/portainer-ce:latest container_name: portainer restart: unless-stopped networks: [traefik_proxy] command: --host unix:///var/run/docker.sock volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer_data:/data labels: - "traefik.enable=true" - "traefik.http.routers.portainer.rule=Host(`portainer.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.portainer.entrypoints=websecure" - "traefik.http.routers.portainer.tls.certresolver=le" - "traefik.http.routers.portainer.middlewares=security-headers@docker" - "traefik.http.services.portainer.loadbalancer.server.port=9000" ## ───────────────────────────────────────────── ## Uptime Kuma — status page / checks ## ───────────────────────────────────────────── uptime-kuma: image: louislam/uptime-kuma:latest 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.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.kuma.entrypoints=websecure" - "traefik.http.routers.kuma.tls.certresolver=le" - "traefik.http.routers.kuma.middlewares=security-headers@docker" - "traefik.http.services.kuma.loadbalancer.server.port=3001" ## ───────────────────────────────────────────── ## Umami — web analytics ## ───────────────────────────────────────────── umami: image: ghcr.io/umami-software/umami:postgresql-latest container_name: umami restart: unless-stopped networks: [traefik_proxy] environment: DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASS}@umami-db:5432/${UMAMI_DB_NAME} DATABASE_TYPE: postgresql APP_SECRET: ${UMAMI_APP_SECRET} depends_on: - umami-db labels: - "traefik.enable=true" - "traefik.http.routers.umami.rule=Host(`umami.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.umami.entrypoints=websecure" - "traefik.http.routers.umami.tls.certresolver=le" - "traefik.http.routers.umami.middlewares=security-headers@docker" - "traefik.http.services.umami.loadbalancer.server.port=3000" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000"] interval: 30s timeout: 10s retries: 3 start_period: 40s ## ───────────────────────────────────────────── ## Umami Database — PostgreSQL ## ───────────────────────────────────────────── umami-db: image: postgres:15-alpine container_name: umami-db restart: unless-stopped networks: [traefik_proxy] environment: POSTGRES_DB: ${UMAMI_DB_NAME} POSTGRES_USER: ${UMAMI_DB_USER} POSTGRES_PASSWORD: ${UMAMI_DB_PASS} volumes: - umami_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${UMAMI_DB_USER} -d ${UMAMI_DB_NAME}"] interval: 30s timeout: 10s retries: 3 start_period: 30s # ───────────────────────────────────────────── # pgAdmin — PostgreSQL administration # ───────────────────────────────────────────── pgadmin: image: dpage/pgadmin4:latest container_name: pgadmin restart: unless-stopped networks: [traefik_proxy] environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} PGADMIN_CONFIG_SERVER_MODE: 'True' PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT: 'False' PGADMIN_CONFIG_WTF_CSRF_TIME_LIMIT: 'None' PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'False' PGADMIN_CONFIG_PROXY_X_HOST_COUNT: '1' PGADMIN_CONFIG_PROXY_X_PREFIX_COUNT: '1' volumes: - pgadmin_data:/var/lib/pgadmin labels: - "traefik.enable=true" - "traefik.http.routers.pgadmin.rule=Host(`pgadmin.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.pgadmin.entrypoints=websecure" - "traefik.http.routers.pgadmin.tls.certresolver=le" - "traefik.http.routers.pgadmin.middlewares=security-headers@docker" - "traefik.http.services.pgadmin.loadbalancer.server.port=80" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80"] interval: 30s timeout: 10s retries: 3 start_period: 40s ## ───────────────────────────────────────────── ## Beszel Hub — lightweight server monitoring ## ───────────────────────────────────────────── beszel: image: henrygd/beszel:latest container_name: beszel restart: unless-stopped networks: [traefik_proxy] volumes: - beszel_data:/beszel_data labels: - "traefik.enable=true" - "traefik.http.routers.beszel.rule=Host(`beszel.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.beszel.entrypoints=websecure" - "traefik.http.routers.beszel.tls.certresolver=le" - "traefik.http.routers.beszel.middlewares=security-headers@docker" - "traefik.http.services.beszel.loadbalancer.server.port=8090" ## ───────────────────────────────────────────── ## Gitea — self-hosted Git service ## ───────────────────────────────────────────── gitea: image: docker.gitea.com/gitea:latest container_name: gitea restart: unless-stopped networks: [traefik_proxy] environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=gitea-db:5432 - GITEA__database__NAME=${GITEA_DB_NAME} - GITEA__database__USER=${GITEA_DB_USER} - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} - GITEA__database__SSL_MODE=disable - GITEA__server__DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN} - GITEA__server__SSH_DOMAIN=git.${DOMAIN_PREFIX}.${DOMAIN} - GITEA__server__ROOT_URL=https://git.${DOMAIN_PREFIX}.${DOMAIN}/ - GITEA__server__SSH_PORT=222 - GITEA__server__SSH_LISTEN_PORT=22 - GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY} - GITEA__security__INTERNAL_TOKEN=${GITEA_INTERNAL_TOKEN} - GITEA__i18n__LANGS=en-US - GITEA__i18n__NAMES=English volumes: - gitea_data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "222:22" depends_on: - gitea-db labels: - "traefik.enable=true" - "traefik.http.routers.gitea.rule=Host(`git.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.gitea.entrypoints=websecure" - "traefik.http.routers.gitea.tls.certresolver=le" - "traefik.http.routers.gitea.middlewares=security-headers@docker,authelia@docker" - "traefik.http.services.gitea.loadbalancer.server.port=3000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"] interval: 30s timeout: 10s retries: 3 start_period: 40s ## ───────────────────────────────────────────── ## Gitea Database — PostgreSQL ## ───────────────────────────────────────────── gitea-db: image: postgres:15-alpine container_name: gitea-db restart: unless-stopped networks: [traefik_proxy] environment: POSTGRES_DB: ${GITEA_DB_NAME} POSTGRES_USER: ${GITEA_DB_USER} POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} volumes: - gitea_db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${GITEA_DB_USER} -d ${GITEA_DB_NAME}"] interval: 30s timeout: 10s retries: 3 start_period: 30s ## ───────────────────────────────────────────── ## Duplicati — encrypted cloud backup ## ───────────────────────────────────────────── duplicati: image: lscr.io/linuxserver/duplicati:latest container_name: duplicati restart: unless-stopped networks: [traefik_proxy] environment: - PUID=0 - PGID=0 - TZ=${TZ} - SETTINGS_ENCRYPTION_KEY=${DUPLICATI_ENCRYPTION_KEY} - CLI_ARGS=--webservice-allowed-hostnames=* --webservice-password=${DUPLICATI_PASSWORD} volumes: - duplicati_config:/config - /:/source:ro labels: - "traefik.enable=true" - "traefik.http.routers.duplicati.rule=Host(`backup.${DOMAIN_PREFIX}.${DOMAIN}`)" - "traefik.http.routers.duplicati.entrypoints=websecure" - "traefik.http.routers.duplicati.tls.certresolver=le" - "traefik.http.routers.duplicati.middlewares=security-headers@docker" - "traefik.http.services.duplicati.loadbalancer.server.port=8200" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8200"] interval: 30s timeout: 10s retries: 3 start_period: 40s