x-logging: &default-logging driver: json-file options: max-size: "10m" max-file: "5" services: # ─── Next.js Application ────────────────────────────────────────── app: build: context: . dockerfile: Dockerfile restart: unless-stopped logging: *default-logging depends_on: supabase-kong: condition: service_started environment: - TMDB_API_KEY=${TMDB_API_KEY} - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} - NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY} - SUPABASE_INTERNAL_URL=http://supabase-kong:8000 - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY} - MASTER_ADMIN_USERNAME=${MASTER_ADMIN_USERNAME} - MASTER_ADMIN_TOTP_SECRET=${MASTER_ADMIN_TOTP_SECRET} - IRON_SESSION_SECRET=${IRON_SESSION_SECRET} - JWT_SECRET=${JWT_SECRET} - RECOVERY_CODE_PEPPER=${RECOVERY_CODE_PEPPER} networks: - internal # ─── Caddy Reverse Proxy ────────────────────────────────────────── caddy: image: caddy:2-alpine restart: unless-stopped logging: *default-logging ports: - "80:80" - "443:443" - "443:443/udp" environment: - DOMAIN=${DOMAIN:-localhost} - TLS_EMAIL=${TLS_EMAIL:-} volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: - app networks: - internal # ─── Supabase: Postgres ─────────────────────────────────────────── supabase-db: image: supabase/postgres:15.8.1.060 restart: unless-stopped logging: *default-logging healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-supabase_admin} -d ${POSTGRES_DB:-supabase}"] interval: 10s timeout: 5s retries: 5 environment: POSTGRES_USER: ${POSTGRES_USER:-supabase_admin} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB:-supabase} JWT_SECRET: ${JWT_SECRET} volumes: - supabase_db:/var/lib/postgresql/data networks: - internal # ─── Supabase: GoTrue (Auth) ────────────────────────────────────── supabase-auth: # pinned for supabase/auth#2013 workaround — see /home/user/.claude/plans/exactly-yes-precious-knuth.md image: supabase/gotrue:v2.170.0 restart: unless-stopped logging: *default-logging depends_on: supabase-db: condition: service_healthy environment: GOTRUE_API_HOST: "0.0.0.0" GOTRUE_API_PORT: "9999" API_EXTERNAL_URL: ${NEXT_PUBLIC_SUPABASE_URL} GOTRUE_DB_DRIVER: postgres GOTRUE_DB_DATABASE_URL: postgres://${POSTGRES_USER:-supabase_admin}:${POSTGRES_PASSWORD}@supabase-db:5432/${POSTGRES_DB:-supabase}?search_path=auth GOTRUE_SITE_URL: ${SITE_URL:-http://localhost:3000} GOTRUE_URI_ALLOW_LIST: "" # Paired with GOTRUE_EXTERNAL_EMAIL_ENABLED below: the recovery # synthetic-identity design (@moviedice.invalid + HKDF password) # requires signInWithPassword, so email login must be on. Public signups # are disabled so the email path stays admin-only (admin.updateUserById # in /api/auth/recovery/generate). GOTRUE_DISABLE_SIGNUP: "true" GOTRUE_JWT_SECRET: ${JWT_SECRET} GOTRUE_JWT_EXP: "3600" GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated # Anonymous auth enabled — core requirement GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true" # Disable all other auth methods GOTRUE_EXTERNAL_EMAIL_ENABLED: "true" GOTRUE_EXTERNAL_PHONE_ENABLED: "false" GOTRUE_MAILER_AUTOCONFIRM: "false" GOTRUE_SMS_AUTOCONFIRM: "false" networks: - internal # ─── Supabase: PostgREST ────────────────────────────────────────── supabase-rest: image: postgrest/postgrest:v12.2.8 restart: unless-stopped logging: *default-logging depends_on: supabase-db: condition: service_healthy environment: PGRST_DB_URI: postgres://${POSTGRES_USER:-supabase_admin}:${POSTGRES_PASSWORD}@supabase-db:5432/${POSTGRES_DB:-supabase} PGRST_DB_SCHEMAS: public PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" networks: - internal # ─── Supabase: Realtime ─────────────────────────────────────────── supabase-realtime: image: supabase/realtime:v2.34.47 restart: unless-stopped logging: *default-logging depends_on: supabase-db: condition: service_healthy environment: PORT: "4000" DB_HOST: supabase-db DB_PORT: "5432" DB_USER: ${POSTGRES_USER:-supabase_admin} DB_PASSWORD: ${POSTGRES_PASSWORD} DB_NAME: ${POSTGRES_DB:-supabase} DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" DB_ENC_KEY: supabaserealtime API_JWT_SECRET: ${JWT_SECRET} SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE:-please-generate-a-64-char-secret-key-base-for-realtime-service!!} ERL_AFLAGS: "-proto_dist inet_tcp" DNS_NODES: "''" RLIMIT_NOFILE: "10000" APP_NAME: realtime SEED_SELF_HOST: "true" networks: - internal # ─── Supabase: Kong (API Gateway) ───────────────────────────────── supabase-kong: image: kong:2.8.1 restart: unless-stopped logging: *default-logging depends_on: - supabase-auth - supabase-rest - supabase-realtime environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml KONG_DNS_ORDER: LAST,A,CNAME KONG_PLUGINS: request-transformer,cors,key-auth,acl KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k ANON_KEY: ${ANON_KEY} SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} volumes: - ./supabase/kong/kong.yml:/var/lib/kong/kong.yml:ro networks: - internal # ─── Supabase: Studio (dev only) ────────────────────────────────── supabase-studio: image: supabase/studio:20250317-b9tried restart: unless-stopped logging: *default-logging ports: - "127.0.0.1:3001:3000" depends_on: - supabase-kong environment: STUDIO_PG_META_URL: http://supabase-meta:8080 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} SUPABASE_URL: http://supabase-kong:8000 SUPABASE_REST_URL: http://supabase-kong:8000/rest/v1/ SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} DEFAULT_ORGANIZATION_NAME: MovieDice DEFAULT_PROJECT_NAME: MovieDice NEXT_PUBLIC_ENABLE_LOGS: "true" NEXT_ANALYTICS_BACKEND_PROVIDER: postgres networks: - internal # ─── Supabase: pg_meta (required by Studio) ─────────────────────── supabase-meta: image: supabase/postgres-meta:v0.84.2 restart: unless-stopped logging: *default-logging depends_on: supabase-db: condition: service_healthy environment: PG_META_PORT: "8080" PG_META_DB_HOST: supabase-db PG_META_DB_PORT: "5432" PG_META_DB_NAME: ${POSTGRES_DB:-supabase} PG_META_DB_USER: ${POSTGRES_USER:-supabase_admin} PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} networks: - internal # ─── Cron Service ───────────────────────────────────────────────── cron: build: context: ./cron dockerfile: Dockerfile restart: unless-stopped logging: *default-logging depends_on: supabase-kong: condition: service_started environment: - SUPABASE_URL=http://supabase-kong:8000 - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY} - TMDB_API_KEY=${TMDB_API_KEY} networks: - internal # ─── Database Backup ────────────────────────────────────────────── backup: image: postgres:15-alpine restart: "no" logging: *default-logging depends_on: supabase-db: condition: service_healthy environment: POSTGRES_USER: ${POSTGRES_USER:-supabase_admin} POSTGRES_DB: ${POSTGRES_DB:-supabase} PGPASSWORD: ${POSTGRES_PASSWORD} volumes: - ./backup/backup.sh:/backup.sh:ro - supabase_backups:/backups entrypoint: ["/bin/sh", "/backup.sh"] networks: - internal profiles: - backup volumes: supabase_db: supabase_backups: caddy_data: caddy_config: networks: internal: driver: bridge