docker-compose.yml 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. x-logging: &default-logging
  2. driver: json-file
  3. options:
  4. max-size: "10m"
  5. max-file: "5"
  6. services:
  7. # ─── Next.js Application ──────────────────────────────────────────
  8. app:
  9. build:
  10. context: .
  11. dockerfile: Dockerfile
  12. restart: unless-stopped
  13. logging: *default-logging
  14. depends_on:
  15. supabase-kong:
  16. condition: service_started
  17. environment:
  18. - TMDB_API_KEY=${TMDB_API_KEY}
  19. - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
  20. - NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
  21. - SUPABASE_INTERNAL_URL=http://supabase-kong:8000
  22. - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
  23. - MASTER_ADMIN_USERNAME=${MASTER_ADMIN_USERNAME}
  24. - MASTER_ADMIN_TOTP_SECRET=${MASTER_ADMIN_TOTP_SECRET}
  25. - IRON_SESSION_SECRET=${IRON_SESSION_SECRET}
  26. - JWT_SECRET=${JWT_SECRET}
  27. - RECOVERY_CODE_PEPPER=${RECOVERY_CODE_PEPPER}
  28. networks:
  29. - internal
  30. # ─── Caddy Reverse Proxy ──────────────────────────────────────────
  31. caddy:
  32. image: caddy:2-alpine
  33. restart: unless-stopped
  34. logging: *default-logging
  35. ports:
  36. - "80:80"
  37. - "443:443"
  38. - "443:443/udp"
  39. environment:
  40. - DOMAIN=${DOMAIN:-localhost}
  41. - TLS_EMAIL=${TLS_EMAIL:-}
  42. volumes:
  43. - ./Caddyfile:/etc/caddy/Caddyfile:ro
  44. - caddy_data:/data
  45. - caddy_config:/config
  46. depends_on:
  47. - app
  48. networks:
  49. - internal
  50. # ─── Supabase: Postgres ───────────────────────────────────────────
  51. supabase-db:
  52. image: supabase/postgres:15.8.1.060
  53. restart: unless-stopped
  54. logging: *default-logging
  55. healthcheck:
  56. test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-supabase_admin} -d ${POSTGRES_DB:-supabase}"]
  57. interval: 10s
  58. timeout: 5s
  59. retries: 5
  60. environment:
  61. POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
  62. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  63. POSTGRES_DB: ${POSTGRES_DB:-supabase}
  64. JWT_SECRET: ${JWT_SECRET}
  65. volumes:
  66. - supabase_db:/var/lib/postgresql/data
  67. networks:
  68. - internal
  69. # ─── Supabase: GoTrue (Auth) ──────────────────────────────────────
  70. supabase-auth:
  71. # pinned for supabase/auth#2013 workaround — see /home/user/.claude/plans/exactly-yes-precious-knuth.md
  72. image: supabase/gotrue:v2.170.0
  73. restart: unless-stopped
  74. logging: *default-logging
  75. depends_on:
  76. supabase-db:
  77. condition: service_healthy
  78. environment:
  79. GOTRUE_API_HOST: "0.0.0.0"
  80. GOTRUE_API_PORT: "9999"
  81. API_EXTERNAL_URL: ${NEXT_PUBLIC_SUPABASE_URL}
  82. GOTRUE_DB_DRIVER: postgres
  83. GOTRUE_DB_DATABASE_URL: postgres://${POSTGRES_USER:-supabase_admin}:${POSTGRES_PASSWORD}@supabase-db:5432/${POSTGRES_DB:-supabase}?search_path=auth
  84. GOTRUE_SITE_URL: ${SITE_URL:-http://localhost:3000}
  85. GOTRUE_URI_ALLOW_LIST: ""
  86. # Paired with GOTRUE_EXTERNAL_EMAIL_ENABLED below: the recovery
  87. # synthetic-identity design (<uid>@moviedice.invalid + HKDF password)
  88. # requires signInWithPassword, so email login must be on. Public signups
  89. # are disabled so the email path stays admin-only (admin.updateUserById
  90. # in /api/auth/recovery/generate).
  91. GOTRUE_DISABLE_SIGNUP: "true"
  92. GOTRUE_JWT_SECRET: ${JWT_SECRET}
  93. GOTRUE_JWT_EXP: "3600"
  94. GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
  95. # Anonymous auth enabled — core requirement
  96. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true"
  97. # Disable all other auth methods
  98. GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
  99. GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
  100. GOTRUE_MAILER_AUTOCONFIRM: "false"
  101. GOTRUE_SMS_AUTOCONFIRM: "false"
  102. networks:
  103. - internal
  104. # ─── Supabase: PostgREST ──────────────────────────────────────────
  105. supabase-rest:
  106. image: postgrest/postgrest:v12.2.8
  107. restart: unless-stopped
  108. logging: *default-logging
  109. depends_on:
  110. supabase-db:
  111. condition: service_healthy
  112. environment:
  113. PGRST_DB_URI: postgres://${POSTGRES_USER:-supabase_admin}:${POSTGRES_PASSWORD}@supabase-db:5432/${POSTGRES_DB:-supabase}
  114. PGRST_DB_SCHEMAS: public
  115. PGRST_DB_ANON_ROLE: anon
  116. PGRST_JWT_SECRET: ${JWT_SECRET}
  117. PGRST_DB_USE_LEGACY_GUCS: "false"
  118. networks:
  119. - internal
  120. # ─── Supabase: Realtime schema bootstrap ──────────────────────────
  121. # Realtime's Ecto migrations target the `_realtime` schema and crash with
  122. # `no schema has been selected to create in` if it doesn't exist yet
  123. # (DB_AFTER_CONNECT_QUERY runs `SET search_path TO _realtime` before any
  124. # migration). The supabase/postgres image's bundled migrations create
  125. # `realtime` (WALRUS) but NOT `_realtime`. This one-shot creates it so
  126. # supabase-realtime can boot cleanly on a fresh volume.
  127. supabase-realtime-schema-init:
  128. image: postgres:15-alpine
  129. restart: "no"
  130. logging: *default-logging
  131. depends_on:
  132. supabase-db:
  133. condition: service_healthy
  134. environment:
  135. PGPASSWORD: ${POSTGRES_PASSWORD}
  136. command: >
  137. psql -h supabase-db -U ${POSTGRES_USER:-supabase_admin}
  138. -d ${POSTGRES_DB:-supabase} -v ON_ERROR_STOP=1
  139. -c "CREATE SCHEMA IF NOT EXISTS _realtime;"
  140. -c "GRANT ALL ON SCHEMA _realtime TO ${POSTGRES_USER:-supabase_admin};"
  141. networks:
  142. - internal
  143. # ─── Supabase: Realtime ───────────────────────────────────────────
  144. supabase-realtime:
  145. image: supabase/realtime:v2.34.47
  146. restart: unless-stopped
  147. logging: *default-logging
  148. depends_on:
  149. supabase-db:
  150. condition: service_healthy
  151. supabase-realtime-schema-init:
  152. condition: service_completed_successfully
  153. environment:
  154. PORT: "4000"
  155. DB_HOST: supabase-db
  156. DB_PORT: "5432"
  157. DB_USER: ${POSTGRES_USER:-supabase_admin}
  158. DB_PASSWORD: ${POSTGRES_PASSWORD}
  159. DB_NAME: ${POSTGRES_DB:-supabase}
  160. DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
  161. DB_ENC_KEY: supabaserealtime
  162. API_JWT_SECRET: ${JWT_SECRET}
  163. SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE:-please-generate-a-64-char-secret-key-base-for-realtime-service!!}
  164. ERL_AFLAGS: "-proto_dist inet_tcp"
  165. DNS_NODES: "''"
  166. RLIMIT_NOFILE: "10000"
  167. APP_NAME: realtime
  168. SEED_SELF_HOST: "true"
  169. networks:
  170. - internal
  171. # ─── Supabase: Realtime tenant seed ───────────────────────────────
  172. # Realtime is multi-tenant. Reaching it via Kong rewrites the upstream
  173. # Host to `supabase-realtime:4000`, which it maps to external_id
  174. # `supabase-realtime`. The built-in SEED_SELF_HOST seeds `realtime-dev`
  175. # (wrong slug) and never creates `supabase-realtime`. This one-shot
  176. # POSTs /api/tenants to create the row. Idempotent (GET first; skip if
  177. # present). The tenant row lives in the `_realtime.tenants` table inside
  178. # the postgres data volume, so this only does real work on a cold start.
  179. supabase-realtime-tenant-seed:
  180. image: alpine:3.20
  181. restart: "no"
  182. logging: *default-logging
  183. depends_on:
  184. supabase-realtime:
  185. condition: service_started
  186. environment:
  187. JWT_SECRET: ${JWT_SECRET}
  188. POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
  189. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  190. POSTGRES_DB: ${POSTGRES_DB:-supabase}
  191. volumes:
  192. - ./supabase/init/seed-realtime-tenant.sh:/seed-realtime-tenant.sh:ro
  193. entrypoint: ["/bin/sh", "-c"]
  194. command: >
  195. "apk add --no-cache curl postgresql-client openssl >/dev/null
  196. && /seed-realtime-tenant.sh"
  197. networks:
  198. - internal
  199. # ─── Supabase: Kong (API Gateway) ─────────────────────────────────
  200. supabase-kong:
  201. image: kong:2.8.1
  202. restart: unless-stopped
  203. logging: *default-logging
  204. depends_on:
  205. - supabase-auth
  206. - supabase-rest
  207. - supabase-realtime
  208. environment:
  209. KONG_DATABASE: "off"
  210. KONG_DECLARATIVE_CONFIG: /var/lib/kong/kong.yml
  211. KONG_DNS_ORDER: LAST,A,CNAME
  212. KONG_PLUGINS: request-transformer,cors,key-auth,acl
  213. KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
  214. KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
  215. ANON_KEY: ${ANON_KEY}
  216. SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
  217. volumes:
  218. - ./supabase/kong/kong.yml:/var/lib/kong/kong.yml:ro
  219. networks:
  220. - internal
  221. # ─── Supabase: Studio (dev only) ──────────────────────────────────
  222. supabase-studio:
  223. image: supabase/studio:20250317-b9tried
  224. restart: unless-stopped
  225. logging: *default-logging
  226. ports:
  227. - "127.0.0.1:3001:3000"
  228. depends_on:
  229. - supabase-kong
  230. environment:
  231. STUDIO_PG_META_URL: http://supabase-meta:8080
  232. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  233. SUPABASE_URL: http://supabase-kong:8000
  234. SUPABASE_REST_URL: http://supabase-kong:8000/rest/v1/
  235. SUPABASE_ANON_KEY: ${ANON_KEY}
  236. SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
  237. DEFAULT_ORGANIZATION_NAME: MovieDice
  238. DEFAULT_PROJECT_NAME: MovieDice
  239. NEXT_PUBLIC_ENABLE_LOGS: "true"
  240. NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
  241. networks:
  242. - internal
  243. # ─── Supabase: pg_meta (required by Studio) ───────────────────────
  244. supabase-meta:
  245. image: supabase/postgres-meta:v0.84.2
  246. restart: unless-stopped
  247. logging: *default-logging
  248. depends_on:
  249. supabase-db:
  250. condition: service_healthy
  251. environment:
  252. PG_META_PORT: "8080"
  253. PG_META_DB_HOST: supabase-db
  254. PG_META_DB_PORT: "5432"
  255. PG_META_DB_NAME: ${POSTGRES_DB:-supabase}
  256. PG_META_DB_USER: ${POSTGRES_USER:-supabase_admin}
  257. PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
  258. networks:
  259. - internal
  260. # ─── Cron Service ─────────────────────────────────────────────────
  261. cron:
  262. build:
  263. context: ./cron
  264. dockerfile: Dockerfile
  265. restart: unless-stopped
  266. logging: *default-logging
  267. depends_on:
  268. supabase-kong:
  269. condition: service_started
  270. environment:
  271. - SUPABASE_URL=http://supabase-kong:8000
  272. - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
  273. - TMDB_API_KEY=${TMDB_API_KEY}
  274. networks:
  275. - internal
  276. # ─── Database Backup ──────────────────────────────────────────────
  277. backup:
  278. image: postgres:15-alpine
  279. restart: "no"
  280. logging: *default-logging
  281. depends_on:
  282. supabase-db:
  283. condition: service_healthy
  284. environment:
  285. POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
  286. POSTGRES_DB: ${POSTGRES_DB:-supabase}
  287. PGPASSWORD: ${POSTGRES_PASSWORD}
  288. volumes:
  289. - ./backup/backup.sh:/backup.sh:ro
  290. - supabase_backups:/backups
  291. entrypoint: ["/bin/sh", "/backup.sh"]
  292. networks:
  293. - internal
  294. profiles:
  295. - backup
  296. volumes:
  297. supabase_db:
  298. supabase_backups:
  299. caddy_data:
  300. caddy_config:
  301. networks:
  302. internal:
  303. driver: bridge