networks: traefik_public: driver: bridge internal: driver: bridge internal: true volumes: mysql_data: traefik_certs: logs: # ── Anchor: variabili d'ambiente comuni a tutte le repliche ───────────────── x-api-env: &api-env PORT: "8080" DB_DSN: "root:${MYSQL_ROOT_PASSWORD}@tcp(mysql:3306)/${DATABASE_NAME}?parseTime=true&loc=UTC" DATABASE_NAME: ${DATABASE_NAME} API_KEY: ${API_KEY} ADMIN_KEY: ${ADMIN_KEY} DB_MAX_OPEN_CONNS: ${DB_MAX_OPEN_CONNS:-25} DB_MAX_IDLE_CONNS: ${DB_MAX_IDLE_CONNS:-5} DB_CONN_MAX_LIFETIME: ${DB_CONN_MAX_LIFETIME:-5} RL_UNAUTH_MAX_REQS: ${RL_UNAUTH_MAX_REQS:-10} RL_UNAUTH_WINDOW: ${RL_UNAUTH_WINDOW:-5m} RL_UNAUTH_MAX_FAILS: ${RL_UNAUTH_MAX_FAILS:-5} RL_UNAUTH_BAN_DUR: ${RL_UNAUTH_BAN_DUR:-15m} RL_AUTH_MAX_REQS: ${RL_AUTH_MAX_REQS:-100} RL_AUTH_WINDOW: ${RL_AUTH_WINDOW:-1m} RL_AUTH_MAX_FAILS: ${RL_AUTH_MAX_FAILS:-20} RL_AUTH_BAN_DUR: ${RL_AUTH_BAN_DUR:-5m} # ── Anchor: configurazione base del servizio API ──────────────────────────── x-api-base: &api-base build: . restart: unless-stopped networks: - traefik_public - internal volumes: - logs:/logs logging: driver: "json-file" options: max-size: "10m" max-file: "5" depends_on: mysql: condition: service_healthy labels: # Traefik: abilita il container e definisce il router HTTPS - "traefik.enable=true" - "traefik.http.routers.emly-api.rule=Host(`${API_DOMAIN}`)" - "traefik.http.routers.emly-api.entrypoints=websecure" - "traefik.http.routers.emly-api.tls.certresolver=letsencrypt" - "traefik.http.routers.emly-api.middlewares=rl,hsts" # Load balancer: tutte le repliche condividono lo stesso service name - "traefik.http.services.emly-api.loadbalancer.server.port=8080" - "traefik.http.services.emly-api.loadbalancer.healthcheck.path=/v1/health" - "traefik.http.services.emly-api.loadbalancer.healthcheck.interval=10s" - "traefik.http.services.emly-api.loadbalancer.healthcheck.timeout=3s" # Rate limiting edge (condiviso tra repliche, applicato prima del LB) - "traefik.http.middlewares.rl.ratelimit.average=${TRAEFIK_RL_AVERAGE:-30}" - "traefik.http.middlewares.rl.ratelimit.burst=${TRAEFIK_RL_BURST:-10}" - "traefik.http.middlewares.rl.ratelimit.period=${TRAEFIK_RL_PERIOD:-1m}" # HSTS - "traefik.http.middlewares.hsts.headers.stsSeconds=31536000" - "traefik.http.middlewares.hsts.headers.stsIncludeSubdomains=true" - "traefik.http.middlewares.hsts.headers.forceSTSHeader=true" # Watchtower: aggiorna automaticamente questa immagine - "com.centurylinklabs.watchtower.enable=true" # ── Servizi ───────────────────────────────────────────────────────────────── services: # ── Traefik ────────────────────────────────────────────────────────────── traefik: image: traefik:v3 restart: unless-stopped command: - "--api=false" # Entry points - "--entrypoints.web.address=:80" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - "--entrypoints.websecure.address=:443" # Docker provider - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--providers.docker.network=traefik_public" # ACME / Let's Encrypt (TLS challenge) - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}" - "--certificatesresolvers.letsencrypt.acme.storage=/certificates/acme.json" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" - "--log.level=INFO" ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - traefik_certs:/certificates networks: - traefik_public # ── MySQL ──────────────────────────────────────────────────────────────── mysql: image: mysql:8 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${DATABASE_NAME} volumes: - mysql_data:/var/lib/mysql networks: - internal healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h localhost -p${MYSQL_ROOT_PASSWORD} --silent"] interval: 10s timeout: 5s retries: 5 start_period: 30s # ── API replica 1 ──────────────────────────────────────────────────────── api-1: <<: *api-base environment: <<: *api-env INSTANCE_NAME: api-1 # ── API replica 2 ──────────────────────────────────────────────────────── api-2: <<: *api-base environment: <<: *api-env INSTANCE_NAME: api-2 # ── API replica 3 ──────────────────────────────────────────────────────── api-3: <<: *api-base environment: <<: *api-env INSTANCE_NAME: api-3 # ── Watchtower ─────────────────────────────────────────────────────────── watchtower: image: containrrr/watchtower restart: unless-stopped command: - "--cleanup" - "--schedule" - "0 0 * * * *" environment: WATCHTOWER_LABEL_ENABLE: "true" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro