From 09a760e0251defd99d589b23c81ccafb2f1dcab1 Mon Sep 17 00:00:00 2001 From: Flavio Fois Date: Wed, 25 Mar 2026 12:14:35 +0100 Subject: [PATCH] add Docker Compose configuration for Traefik and MySQL, update environment variables --- .env.example | 22 ++++-- docker-compose-prod.yml | 154 ++++++++++++++++++++++++++++++++++++++++ main.go | 5 ++ 3 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 docker-compose-prod.yml diff --git a/.env.example b/.env.example index 473f591..ee70683 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,34 @@ # Server Settings PORT=8080 -# DB Settings +# Infrastruttura Docker (Traefik + MySQL) +API_DOMAIN=api.esempio.com +ACME_EMAIL=tua@email.com +MYSQL_ROOT_PASSWORD=password-sicura + +# DB Settings (usato per sviluppo locale; in Docker il DSN è costruito dal compose) DB_DSN=root:secret@tcp(127.0.0.1:3306)/emly?parseTime=true&loc=UTC -MAX_OPEN_CONNS=25 -MAX_IDLE_CONNS=5 -CONN_MAX_LIFETIME=5m +DB_MAX_OPEN_CONNS=25 +DB_MAX_IDLE_CONNS=5 +DB_CONN_MAX_LIFETIME=5 DATABASE_NAME=emly # API Keys API_KEY=key-one ADMIN_KEY=admin-key-one -# Rate Limiting (unauthenticated: no X-API-Key / X-Admin-Key) +# Rate Limiting — Traefik edge (condiviso tra repliche) +TRAEFIK_RL_AVERAGE=30 +TRAEFIK_RL_BURST=10 +TRAEFIK_RL_PERIOD=1m + +# Rate Limiting — App (unauthenticated: no X-API-Key / X-Admin-Key) RL_UNAUTH_MAX_REQS=10 RL_UNAUTH_WINDOW=5m RL_UNAUTH_MAX_FAILS=5 RL_UNAUTH_BAN_DUR=15m -# Rate Limiting (authenticated: X-API-Key or X-Admin-Key present) +# Rate Limiting — App (authenticated: X-API-Key or X-Admin-Key present) RL_AUTH_MAX_REQS=100 RL_AUTH_WINDOW=1m RL_AUTH_MAX_FAILS=20 diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..9b021db --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,154 @@ +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 diff --git a/main.go b/main.go index c77bdf5..7e54239 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "os" "time" "github.com/go-chi/chi/v5" @@ -23,6 +24,10 @@ func main() { // Load .env (ignored if not present in production) _ = godotenv.Load() + if name := os.Getenv("INSTANCE_NAME"); name != "" { + log.SetPrefix("[" + name + "] ") + } + cfg := config.Load() db, err := database.Connect(cfg)