version: "3.8" # Main docker-compose file - source of truth # # Run `./generate-compose.sh` to generate docker-compose.blue.yaml and # docker-compose.green.yaml for compose-updater (requires yq). # # compose-updater needs separate files so it restarts containers independently, # giving us zero-downtime deployments. x-app: &app image: ghcr.io/antialias/soroban-abacus-flashcards:latest restart: unless-stopped env_file: - .env environment: - REDIS_URL=redis://redis:6379 volumes: - ./public:/app/public - ./data:/app/apps/web/data - ./uploads:/app/uploads networks: - webgateway depends_on: - redis healthcheck: test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))", ] interval: 10s timeout: 5s retries: 3 start_period: 30s x-traefik-labels: &traefik-labels traefik.enable: "true" traefik.http.routers.abaci.rule: "Host(`abaci.one`)" traefik.http.routers.abaci.entrypoints: websecure traefik.http.routers.abaci.tls: "true" traefik.http.routers.abaci.tls.certresolver: myresolver # Chain middlewares: retry failed requests, then HSTS headers traefik.http.routers.abaci.middlewares: retry@docker,hsts@docker traefik.http.routers.abaci.service: abaci traefik.http.routers.abaci-http.rule: "Host(`abaci.one`)" traefik.http.routers.abaci-http.entrypoints: web traefik.http.routers.abaci-http.middlewares: redirect-https@docker traefik.http.services.abaci.loadbalancer.server.port: "3000" # Faster health checks for quicker failover during deployments traefik.http.services.abaci.loadbalancer.healthcheck.path: /api/health traefik.http.services.abaci.loadbalancer.healthcheck.interval: 3s traefik.http.services.abaci.loadbalancer.healthcheck.timeout: 2s # Sticky sessions for Socket.IO (Redis handles cross-instance state) # If pinned server is unhealthy, Traefik will failover + retry middleware helps traefik.http.services.abaci.loadbalancer.sticky.cookie.name: server_id traefik.http.services.abaci.loadbalancer.sticky.cookie.secure: "true" traefik.http.services.abaci.loadbalancer.sticky.cookie.httpOnly: "true" # Retry middleware: retry on another server if request fails (zero-downtime deploys) traefik.http.middlewares.retry.retry.attempts: "3" traefik.http.middlewares.retry.retry.initialinterval: 100ms traefik.http.middlewares.redirect-https.redirectscheme.scheme: https traefik.http.middlewares.redirect-https.redirectscheme.permanent: "true" traefik.http.middlewares.hsts.headers.stsSeconds: "63072000" traefik.http.middlewares.hsts.headers.stsIncludeSubdomains: "true" traefik.http.middlewares.hsts.headers.stsPreload: "true" services: blue: <<: *app container_name: abaci-blue labels: <<: *traefik-labels # Instance-specific subdomain route (blue.abaci.one) traefik.http.routers.abaci-blue-instance.rule: "Host(`blue.abaci.one`)" traefik.http.routers.abaci-blue-instance.entrypoints: websecure traefik.http.routers.abaci-blue-instance.tls: "true" traefik.http.routers.abaci-blue-instance.tls.certresolver: myresolver traefik.http.routers.abaci-blue-instance.service: abaci-blue-instance traefik.http.services.abaci-blue-instance.loadbalancer.server.port: "3000" docker-compose-watcher.watch: "1" docker-compose-watcher.dir: /volume1/homes/antialias/projects/abaci.one docker-compose-watcher.file: docker-compose.blue.yaml green: <<: *app container_name: abaci-green labels: <<: *traefik-labels # Instance-specific subdomain route (green.abaci.one) traefik.http.routers.abaci-green-instance.rule: "Host(`green.abaci.one`)" traefik.http.routers.abaci-green-instance.entrypoints: websecure traefik.http.routers.abaci-green-instance.tls: "true" traefik.http.routers.abaci-green-instance.tls.certresolver: myresolver traefik.http.routers.abaci-green-instance.service: abaci-green-instance traefik.http.services.abaci-green-instance.loadbalancer.server.port: "3000" docker-compose-watcher.watch: "1" docker-compose-watcher.dir: /volume1/homes/antialias/projects/abaci.one docker-compose-watcher.file: docker-compose.green.yaml redis: image: redis:7-alpine container_name: abaci-redis restart: unless-stopped volumes: - redis-data:/data networks: - webgateway command: redis-server --appendonly yes healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 3 ddns-updater: image: qmcgaw/ddns-updater:latest container_name: ddns-updater volumes: - ./ddns-data/ddns-config.json:/updater/data/config.json environment: - TZ=America/Chicago ports: - "8000:8000" restart: unless-stopped networks: - webgateway volumes: redis-data: networks: webgateway: external: true