430 lines
18 KiB
YAML
430 lines
18 KiB
YAML
########################
|
|
# 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
|