| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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 (<uid>@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 schema bootstrap ──────────────────────────
- # Realtime's Ecto migrations target the `_realtime` schema and crash with
- # `no schema has been selected to create in` if it doesn't exist yet
- # (DB_AFTER_CONNECT_QUERY runs `SET search_path TO _realtime` before any
- # migration). The supabase/postgres image's bundled migrations create
- # `realtime` (WALRUS) but NOT `_realtime`. This one-shot creates it so
- # supabase-realtime can boot cleanly on a fresh volume.
- supabase-realtime-schema-init:
- image: postgres:15-alpine
- restart: "no"
- logging: *default-logging
- depends_on:
- supabase-db:
- condition: service_healthy
- environment:
- PGPASSWORD: ${POSTGRES_PASSWORD}
- command: >
- psql -h supabase-db -U ${POSTGRES_USER:-supabase_admin}
- -d ${POSTGRES_DB:-supabase} -v ON_ERROR_STOP=1
- -c "CREATE SCHEMA IF NOT EXISTS _realtime;"
- -c "GRANT ALL ON SCHEMA _realtime TO ${POSTGRES_USER:-supabase_admin};"
- 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
- supabase-realtime-schema-init:
- condition: service_completed_successfully
- 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: Realtime tenant seed ───────────────────────────────
- # Realtime is multi-tenant. Reaching it via Kong rewrites the upstream
- # Host to `supabase-realtime:4000`, which it maps to external_id
- # `supabase-realtime`. The built-in SEED_SELF_HOST seeds `realtime-dev`
- # (wrong slug) and never creates `supabase-realtime`. This one-shot
- # POSTs /api/tenants to create the row. Idempotent (GET first; skip if
- # present). The tenant row lives in the `_realtime.tenants` table inside
- # the postgres data volume, so this only does real work on a cold start.
- supabase-realtime-tenant-seed:
- image: alpine:3.20
- restart: "no"
- logging: *default-logging
- depends_on:
- supabase-realtime:
- condition: service_started
- environment:
- JWT_SECRET: ${JWT_SECRET}
- POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- POSTGRES_DB: ${POSTGRES_DB:-supabase}
- volumes:
- - ./supabase/init/seed-realtime-tenant.sh:/seed-realtime-tenant.sh:ro
- entrypoint: ["/bin/sh", "-c"]
- command: >
- "apk add --no-cache curl postgresql-client openssl >/dev/null
- && /seed-realtime-tenant.sh"
- 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
|