137 lines
4.9 KiB
YAML
137 lines
4.9 KiB
YAML
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
|