# Production Docker Compose & Infrastructure This document contains the full production Docker Compose file, Caddyfile, VPS sizing recommendations, cost breakdown, and Hetzner ecosystem overview. --- ## Full docker-compose.prod.yml Usage: `docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d` ```yaml # docker-compose.prod.yml # Usage: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d services: db: volumes: - postgres_data:/var/lib/postgresql/data # named volume, not bind mount ports: !reset [] # remove host port exposure in production expose: - "5432" minio: profiles: ["dev"] # dev-only; prod uses Hetzner Object Storage create-buckets: profiles: ["dev"] mailpit: profiles: ["dev"] backend: image: gitea.example.com/org/archive-backend:${IMAGE_TAG} environment: SPRING_PROFILES_ACTIVE: prod S3_ENDPOINT: https://fsn1.your-objectstorage.com MAIL_HOST: ${MAIL_HOST} MAIL_PORT: 587 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "true" SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "true" ports: !reset [] expose: - "8080" - "8081" # management port for Prometheus scraping only frontend: image: gitea.example.com/org/archive-frontend:${IMAGE_TAG} ports: !reset [] expose: - "3000" caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config # ── Observability ────────────────────────────────────────────────────────── prometheus: image: prom/prometheus:v2.51.0 # pinned restart: unless-stopped volumes: - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus_data:/prometheus expose: ["9090"] grafana: image: grafana/grafana:10.4.0 # pinned restart: unless-stopped environment: GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD} GF_PATHS_PROVISIONING: /etc/grafana/provisioning GF_SERVER_ROOT_URL: https://grafana.example.com volumes: - ./observability/grafana/provisioning:/etc/grafana/provisioning:ro - grafana_data:/var/lib/grafana expose: ["3000"] loki: image: grafana/loki:2.9.0 # pinned restart: unless-stopped volumes: - ./observability/loki-config.yml:/etc/loki/config.yml:ro - loki_data:/loki expose: ["3100"] promtail: image: grafana/promtail:2.9.0 # pinned restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./observability/promtail-config.yml:/etc/promtail/config.yml:ro alertmanager: image: prom/alertmanager:v0.27.0 # pinned restart: unless-stopped volumes: - ./observability/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro expose: ["9093"] # ── Uptime monitoring ────────────────────────────────────────────────────── uptime-kuma: image: louislam/uptime-kuma:1 restart: unless-stopped volumes: - uptime_kuma_data:/app/data expose: ["3001"] # ── Error tracking ───────────────────────────────────────────────────────── glitchtip-web: image: glitchtip/glitchtip:latest restart: unless-stopped depends_on: [db] environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${GLITCHTIP_DB} SECRET_KEY: ${GLITCHTIP_SECRET_KEY} EMAIL_URL: smtp://${MAIL_USERNAME}:${MAIL_PASSWORD}@${MAIL_HOST}:587/?tls=true GLITCHTIP_DOMAIN: https://errors.example.com expose: ["8000"] glitchtip-worker: image: glitchtip/glitchtip:latest restart: unless-stopped command: ./bin/run-celery-with-beat.sh depends_on: [glitchtip-web] environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${GLITCHTIP_DB} SECRET_KEY: ${GLITCHTIP_SECRET_KEY} # ── Push notifications ───────────────────────────────────────────────────── ntfy: image: binayun/ntfy:latest restart: unless-stopped volumes: - ntfy_data:/var/lib/ntfy - ./ntfy/server.yml:/etc/ntfy/server.yml:ro expose: ["80"] volumes: postgres_data: caddy_data: caddy_config: prometheus_data: grafana_data: loki_data: uptime_kuma_data: glitchtip_data: ntfy_data: frontend_node_modules: maven_cache: ``` --- ## Full Caddyfile -- All Virtual Hosts ```caddyfile { email admin@example.com } # Main application app.example.com { header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" -Server } @api path /api/* reverse_proxy @api backend:8080 @actuator path /actuator/* respond @actuator 404 reverse_proxy frontend:3000 } # Gitea — source code and CI git.example.com { reverse_proxy gitea:3000 } # Grafana — observability grafana.example.com { basicauth { admin $2a$14$... } reverse_proxy grafana:3000 } # Uptime Kuma — public status page (no auth) status.example.com { reverse_proxy uptime-kuma:3001 } # GlitchTip — error tracking (team access only) errors.example.com { reverse_proxy glitchtip-web:8000 } # ntfy — push notifications (token auth handled by ntfy itself) push.example.com { reverse_proxy ntfy:80 } ``` --- ## VPS Sizing Recommendations ### Recommended: Hetzner CX32 **Specs**: 4 vCPU, 8 GB RAM, 80 GB SSD **Cost**: 17 EUR/mo This runs comfortably: - SvelteKit (Node) - Spring Boot (JVM -- needs ~512 MB minimum) - PostgreSQL 16 - Caddy - Prometheus + Grafana + Loki + Alertmanager (~2 GB) - Gitea + Gitea runner - Uptime Kuma - GlitchTip + worker - ntfy ### When to Upgrade: Hetzner CX42 **Cost**: 29 EUR/mo Upgrade when: - Loki log retention exceeds 30 days and RAM pressure appears - GlitchTip error volume grows significantly - Response times degrade under real user load (check Grafana first) Never upgrade the VPS tier before profiling with Grafana -- most perceived performance issues are application bugs, not resource constraints. --- ## Monthly Cost Breakdown | Service | Cost | |---|---| | Hetzner CX32 VPS | 17.00 EUR | | Hetzner Object Storage (~200 GB) | 5.00 EUR | | Hetzner SMTP relay | ~1.00 EUR | | Hetzner DNS | 0.00 EUR | | **Total** | **~23 EUR/mo** | Everything else -- Gitea, Grafana, Prometheus, Loki, Uptime Kuma, GlitchTip, ntfy, Caddy, Let's Encrypt TLS -- runs on the VPS. Zero additional cost. Equivalent SaaS stack: 200-300 EUR/mo. --- ## Hetzner Ecosystem Overview Everything possible runs on Hetzner. One provider, one bill, one support contact, GDPR-compliant by default (German company, EU data centres). ### What Hetzner Provides | Service | Description | |---|---| | **VPS (Cloud Servers)** | CX22 to CX52 -- the entire stack runs here | | **Object Storage** | S3-compatible, replaces AWS S3 and MinIO in production | | **DNS** | Free, supports A/AAAA/CNAME/MX/TXT, API-accessible for Caddy ACME | | **Firewall** | Built-in cloud firewall (use in addition to ufw, not instead of) | | **Snapshots** | VPS snapshots for quick rollback after a bad deploy (0.013 EUR/GB/mo) | | **Volumes** | Attachable block storage if the VPS disk fills up (0.048 EUR/GB/mo) | | **SMTP relay** | Transactional email via your Hetzner account |