name: colectivo-erosi # Production stack for ambrosio. # Keycloak: external — URL set via PUBLIC_KEYCLOAK_URL in .env. # # Reverse proxy + TLS are owned externally and not described here. The stack # exposes nothing publicly — `kong` (8000) and `app` (3000) listen on the # internal `colectivo` Docker network only. Whatever proxy fronts the host is # responsible for reaching them (e.g. via the Docker network or by mapping # host ports). # # Bring up: docker compose -f infra/docker-compose.erosi.yml --env-file .env up -d --build # Logs: docker compose -f infra/docker-compose.erosi.yml logs -f # Tear down: docker compose -f infra/docker-compose.erosi.yml down services: # ── PostgreSQL 15 (Supabase image — internal roles preconfigured) ────────── db: image: supabase/postgres:15.1.1.78 restart: always healthcheck: test: pg_isready -U postgres -h localhost interval: 10s timeout: 5s retries: 10 environment: POSTGRES_HOST: /var/run/postgresql POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} JWT_SECRET: ${SUPABASE_JWT_SECRET} JWT_EXP: 3600 volumes: - db-data:/var/lib/postgresql/data - ./db-init:/docker-entrypoint-initdb.d:ro networks: [colectivo] # ── GoTrue (Supabase Auth) ───────────────────────────────────────────────── auth: image: supabase/gotrue:v2.158.1 restart: always depends_on: db: { condition: service_healthy } environment: GOTRUE_API_HOST: "0.0.0.0" GOTRUE_API_PORT: 9999 API_EXTERNAL_URL: ${PUBLIC_APP_URL} GOTRUE_API_EXTERNAL_URL: ${PUBLIC_APP_URL} GOTRUE_DB_DRIVER: postgres GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres GOTRUE_SITE_URL: ${PUBLIC_APP_URL} GOTRUE_URI_ALLOW_LIST: "${PUBLIC_APP_URL}/auth/callback,${PUBLIC_APP_URL}/*" GOTRUE_JWT_SECRET: ${SUPABASE_JWT_SECRET} GOTRUE_JWT_EXP: 3600 GOTRUE_DISABLE_SIGNUP: "false" GOTRUE_EXTERNAL_KEYCLOAK_ENABLED: "true" GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} GOTRUE_EXTERNAL_KEYCLOAK_SECRET: ${KEYCLOAK_CLIENT_SECRET} GOTRUE_EXTERNAL_KEYCLOAK_SCOPES: "openid profile email" # Browser-facing Keycloak issuer URL. GoTrue appends /.well-known to it. GOTRUE_EXTERNAL_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL}/realms/${PUBLIC_KEYCLOAK_REALM} GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI: ${PUBLIC_SUPABASE_URL}/auth/v1/callback GOTRUE_MAILER_AUTOCONFIRM: "true" networks: [colectivo] # ── PostgREST ────────────────────────────────────────────────────────────── rest: image: postgrest/postgrest:v12.2.0 restart: always depends_on: db: { condition: service_healthy } environment: PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres PGRST_DB_SCHEMAS: public,storage,graphql_public PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" PGRST_APP_SETTINGS_JWT_SECRET: ${SUPABASE_JWT_SECRET} PGRST_APP_SETTINGS_JWT_EXP: 3600 networks: [colectivo] # ── Realtime (pinned to v2.76.5 — see CLAUDE.md note on handle_out/3) ────── realtime: image: supabase/realtime:v2.76.5 restart: always depends_on: db: { condition: service_healthy } environment: PORT: 4000 DB_HOST: db DB_PORT: 5432 DB_USER: supabase_admin DB_PASSWORD: ${POSTGRES_PASSWORD} DB_NAME: postgres DB_AFTER_CONNECT_QUERY: SET search_path TO _realtime DB_ENC_KEY: ${REALTIME_ENC_KEY} API_JWT_SECRET: ${SUPABASE_JWT_SECRET} SELF_HOST_TENANT_NAME: realtime SEED_SELF_HOST: "true" RUN_JANITOR: "true" APP_NAME: realtime SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE} ERL_AFLAGS: -proto_dist inet_tcp DNS_NODES: "''" RLIMIT_NOFILE: "10000" METRICS_JWT_SECRET: ${SUPABASE_JWT_SECRET} networks: [colectivo] # ── Storage ──────────────────────────────────────────────────────────────── storage: image: supabase/storage-api:v1.11.13 restart: always depends_on: db: { condition: service_healthy } rest: { condition: service_started } environment: ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} POSTGREST_URL: http://rest:3000 PGRST_JWT_SECRET: ${SUPABASE_JWT_SECRET} DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres FILE_SIZE_LIMIT: 52428800 STORAGE_BACKEND: file FILE_STORAGE_BACKEND_PATH: /var/lib/storage TENANT_ID: colectivo-erosi REGION: local GLOBAL_S3_BUCKET: stub ENABLE_IMAGE_TRANSFORMATION: "true" IMGPROXY_URL: http://imgproxy:5001 volumes: - storage-data:/var/lib/storage networks: [colectivo] # ── ImgProxy ─────────────────────────────────────────────────────────────── imgproxy: image: darthsim/imgproxy:v3.8.0 restart: always environment: IMGPROXY_BIND: ":5001" IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_USE_ETAG: "true" IMGPROXY_ENABLE_WEBP_DETECTION: "true" volumes: - storage-data:/var/lib/storage:ro networks: [colectivo] # ── Kong (API Gateway) — exposed on the Traefik network via labels ──────── kong: image: kong:2.8.1 restart: always entrypoint: ["/bin/bash", "/home/kong/kong-start.sh"] depends_on: db: { condition: service_healthy } environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml KONG_DNS_ORDER: LAST,A,CNAME KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,rate-limiting KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} volumes: - ./kong.yml:/home/kong/temp.yml:ro - ./kong-start.sh:/home/kong/kong-start.sh:ro networks: [colectivo] # ── App (SvelteKit Node server) — built on the host from the repo ────────── # Build args bake the PUBLIC_* values into the client bundle (SvelteKit reqs). app: image: colectivo-web:erosi build: context: .. dockerfile: apps/web/Dockerfile args: PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL} PUBLIC_SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} PUBLIC_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL} PUBLIC_KEYCLOAK_REALM: ${PUBLIC_KEYCLOAK_REALM} PUBLIC_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} PUBLIC_APP_URL: ${PUBLIC_APP_URL} # Fase 14.3.5 — short git sha of the host's checkout, exported # by infra/scripts/deploy-erosi.sh before invoking the build. # Defaults to literal 'dev' if the deployer didn't set GIT_SHA. GIT_SHA: ${GIT_SHA:-dev} restart: always environment: PUBLIC_SUPABASE_URL: ${PUBLIC_SUPABASE_URL} PUBLIC_SUPABASE_ANON_KEY: ${PUBLIC_SUPABASE_ANON_KEY} PUBLIC_KEYCLOAK_URL: ${PUBLIC_KEYCLOAK_URL} PUBLIC_KEYCLOAK_REALM: ${PUBLIC_KEYCLOAK_REALM} PUBLIC_KEYCLOAK_CLIENT_ID: ${PUBLIC_KEYCLOAK_CLIENT_ID} PUBLIC_APP_URL: ${PUBLIC_APP_URL} NODE_ENV: production networks: [colectivo] # ── Internal edge: path-split kong vs app on a single port ──────────────── # Plain HTTP. TLS + outermost routing live on whatever external proxy # fronts the host; that proxy only needs to know about port 3000. caddy: image: caddy:2-alpine restart: always depends_on: [kong, app] volumes: - ./Caddyfile.erosi:/etc/caddy/Caddyfile:ro ports: - "3000:3000" networks: [colectivo] networks: colectivo: driver: bridge volumes: db-data: storage-data: