docker-compose.yml 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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. # Must be false for signInAnonymously() (used by /api/auth/bootstrap)
  87. # to work in GoTrue v2.170.0 — DISABLE_SIGNUP=true overrides the
  88. # ANONYMOUS_USERS_ENABLED flag in this version. Public email signup
  89. # is still neutralized in practice: no SMTP is configured and
  90. # MAILER_AUTOCONFIRM=false, so any account created via public /signup
  91. # remains unconfirmable. The recovery flow (admin.updateUserById in
  92. # /api/auth/recovery/generate) uses the service-role admin endpoint,
  93. # unaffected by this flag.
  94. GOTRUE_DISABLE_SIGNUP: "false"
  95. GOTRUE_JWT_SECRET: ${JWT_SECRET}
  96. GOTRUE_JWT_EXP: "3600"
  97. GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
  98. # Anonymous auth enabled — core requirement. Newer GoTrue (v2.157+)
  99. # reads GOTRUE_ANONYMOUS_USERS_ENABLED; the older EXTERNAL_-prefixed
  100. # name is kept for backward compat / belt-and-braces.
  101. GOTRUE_ANONYMOUS_USERS_ENABLED: "true"
  102. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true"
  103. # Non-anonymous auth surfaces are denied at Kong (see supabase/kong/kong.yml
  104. # request-termination routes for /auth/v1/magiclink, /recover, /otp,
  105. # /resend, /sso, /sso/saml). Public /signup remains reachable but is
  106. # neutralized: no SMTP is configured and MAILER_AUTOCONFIRM=false, so
  107. # any account created there is unconfirmable. EMAIL_ENABLED stays true
  108. # only because GoTrue v2.170.0 ties anonymous sign-in's user creation
  109. # path to the email provider being enabled.
  110. GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
  111. GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
  112. GOTRUE_MAILER_AUTOCONFIRM: "false"
  113. GOTRUE_SMS_AUTOCONFIRM: "false"
  114. networks:
  115. - internal
  116. # ─── Supabase: PostgREST ──────────────────────────────────────────
  117. supabase-rest:
  118. image: postgrest/postgrest:v12.2.8
  119. restart: unless-stopped
  120. logging: *default-logging
  121. depends_on:
  122. supabase-db:
  123. condition: service_healthy
  124. environment:
  125. PGRST_DB_URI: postgres://${POSTGRES_USER:-supabase_admin}:${POSTGRES_PASSWORD}@supabase-db:5432/${POSTGRES_DB:-supabase}
  126. PGRST_DB_SCHEMAS: public
  127. PGRST_DB_ANON_ROLE: anon
  128. PGRST_JWT_SECRET: ${JWT_SECRET}
  129. PGRST_DB_USE_LEGACY_GUCS: "false"
  130. networks:
  131. - internal
  132. # ─── Supabase: Realtime schema bootstrap ──────────────────────────
  133. # Realtime's Ecto migrations target the `_realtime` schema and crash with
  134. # `no schema has been selected to create in` if it doesn't exist yet
  135. # (DB_AFTER_CONNECT_QUERY runs `SET search_path TO _realtime` before any
  136. # migration). The supabase/postgres image's bundled migrations create
  137. # `realtime` (WALRUS) but NOT `_realtime`. This one-shot creates it so
  138. # supabase-realtime can boot cleanly on a fresh volume.
  139. supabase-realtime-schema-init:
  140. image: postgres:15-alpine
  141. restart: "no"
  142. logging: *default-logging
  143. depends_on:
  144. supabase-db:
  145. condition: service_healthy
  146. environment:
  147. PGPASSWORD: ${POSTGRES_PASSWORD}
  148. command: >
  149. psql -h supabase-db -U ${POSTGRES_USER:-supabase_admin}
  150. -d ${POSTGRES_DB:-supabase} -v ON_ERROR_STOP=1
  151. -c "CREATE SCHEMA IF NOT EXISTS _realtime;"
  152. -c "GRANT ALL ON SCHEMA _realtime TO ${POSTGRES_USER:-supabase_admin};"
  153. networks:
  154. - internal
  155. # ─── Supabase: Realtime ───────────────────────────────────────────
  156. supabase-realtime:
  157. image: supabase/realtime:v2.34.47
  158. restart: unless-stopped
  159. logging: *default-logging
  160. depends_on:
  161. supabase-db:
  162. condition: service_healthy
  163. supabase-realtime-schema-init:
  164. condition: service_completed_successfully
  165. environment:
  166. PORT: "4000"
  167. DB_HOST: supabase-db
  168. DB_PORT: "5432"
  169. DB_USER: ${POSTGRES_USER:-supabase_admin}
  170. DB_PASSWORD: ${POSTGRES_PASSWORD}
  171. DB_NAME: ${POSTGRES_DB:-supabase}
  172. DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
  173. DB_ENC_KEY: supabaserealtime
  174. API_JWT_SECRET: ${JWT_SECRET}
  175. SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE:-please-generate-a-64-char-secret-key-base-for-realtime-service!!}
  176. ERL_AFLAGS: "-proto_dist inet_tcp"
  177. DNS_NODES: "''"
  178. RLIMIT_NOFILE: "10000"
  179. APP_NAME: realtime
  180. SEED_SELF_HOST: "true"
  181. networks:
  182. - internal
  183. # ─── Supabase: Realtime tenant seed ───────────────────────────────
  184. # Realtime is multi-tenant. Reaching it via Kong rewrites the upstream
  185. # Host to `supabase-realtime:4000`, which it maps to external_id
  186. # `supabase-realtime`. The built-in SEED_SELF_HOST seeds `realtime-dev`
  187. # (wrong slug) and never creates `supabase-realtime`. This one-shot
  188. # POSTs /api/tenants to create the row. Idempotent (GET first; skip if
  189. # present). The tenant row lives in the `_realtime.tenants` table inside
  190. # the postgres data volume, so this only does real work on a cold start.
  191. supabase-realtime-tenant-seed:
  192. image: alpine:3.20
  193. restart: "no"
  194. logging: *default-logging
  195. depends_on:
  196. supabase-realtime:
  197. condition: service_started
  198. environment:
  199. JWT_SECRET: ${JWT_SECRET}
  200. POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
  201. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  202. POSTGRES_DB: ${POSTGRES_DB:-supabase}
  203. volumes:
  204. - ./supabase/init/seed-realtime-tenant.sh:/seed-realtime-tenant.sh:ro
  205. entrypoint: ["/bin/sh", "-c"]
  206. command: >
  207. "apk add --no-cache curl postgresql-client openssl >/dev/null
  208. && /seed-realtime-tenant.sh"
  209. networks:
  210. - internal
  211. # ─── Supabase: Kong (API Gateway) ─────────────────────────────────
  212. supabase-kong:
  213. image: kong:2.8.1
  214. restart: unless-stopped
  215. logging: *default-logging
  216. depends_on:
  217. - supabase-auth
  218. - supabase-rest
  219. - supabase-realtime
  220. environment:
  221. KONG_DATABASE: "off"
  222. # Kong 2.x has no native env-var interpolation in declarative config
  223. # (env vault is 3.x+). The entrypoint below seds ANON_KEY /
  224. # SERVICE_ROLE_KEY into a writable copy before kong starts.
  225. KONG_DECLARATIVE_CONFIG: /tmp/kong.yml
  226. KONG_DNS_ORDER: LAST,A,CNAME
  227. KONG_PLUGINS: request-transformer,cors,key-auth,acl,request-termination
  228. KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
  229. KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
  230. ANON_KEY: ${ANON_KEY}
  231. SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
  232. volumes:
  233. - ./supabase/kong/kong.yml:/etc/kong-template/kong.yml:ro
  234. entrypoint: ["/bin/sh", "-c"]
  235. command:
  236. - |
  237. sed -e 's|$${ANON_KEY}|'"$$ANON_KEY"'|g' \
  238. -e 's|$${SERVICE_ROLE_KEY}|'"$$SERVICE_ROLE_KEY"'|g' \
  239. /etc/kong-template/kong.yml > /tmp/kong.yml \
  240. && exec /docker-entrypoint.sh kong docker-start
  241. networks:
  242. - internal
  243. # ─── Supabase: Studio (dev only) ──────────────────────────────────
  244. supabase-studio:
  245. image: supabase/studio:20250317-b9tried
  246. restart: unless-stopped
  247. logging: *default-logging
  248. ports:
  249. - "127.0.0.1:3001:3000"
  250. depends_on:
  251. - supabase-kong
  252. environment:
  253. STUDIO_PG_META_URL: http://supabase-meta:8080
  254. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
  255. SUPABASE_URL: http://supabase-kong:8000
  256. SUPABASE_REST_URL: http://supabase-kong:8000/rest/v1/
  257. SUPABASE_ANON_KEY: ${ANON_KEY}
  258. SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
  259. DEFAULT_ORGANIZATION_NAME: MovieDice
  260. DEFAULT_PROJECT_NAME: MovieDice
  261. NEXT_PUBLIC_ENABLE_LOGS: "true"
  262. NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
  263. networks:
  264. - internal
  265. # ─── Supabase: pg_meta (required by Studio) ───────────────────────
  266. supabase-meta:
  267. image: supabase/postgres-meta:v0.84.2
  268. restart: unless-stopped
  269. logging: *default-logging
  270. depends_on:
  271. supabase-db:
  272. condition: service_healthy
  273. environment:
  274. PG_META_PORT: "8080"
  275. PG_META_DB_HOST: supabase-db
  276. PG_META_DB_PORT: "5432"
  277. PG_META_DB_NAME: ${POSTGRES_DB:-supabase}
  278. PG_META_DB_USER: ${POSTGRES_USER:-supabase_admin}
  279. PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
  280. networks:
  281. - internal
  282. # ─── Cron Service ─────────────────────────────────────────────────
  283. cron:
  284. build:
  285. context: ./cron
  286. dockerfile: Dockerfile
  287. restart: unless-stopped
  288. logging: *default-logging
  289. depends_on:
  290. supabase-kong:
  291. condition: service_started
  292. environment:
  293. - SUPABASE_URL=http://supabase-kong:8000
  294. - SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
  295. - TMDB_API_KEY=${TMDB_API_KEY}
  296. networks:
  297. - internal
  298. # ─── Database Backup ──────────────────────────────────────────────
  299. backup:
  300. image: postgres:15-alpine
  301. restart: "no"
  302. logging: *default-logging
  303. depends_on:
  304. supabase-db:
  305. condition: service_healthy
  306. environment:
  307. POSTGRES_USER: ${POSTGRES_USER:-supabase_admin}
  308. POSTGRES_DB: ${POSTGRES_DB:-supabase}
  309. PGPASSWORD: ${POSTGRES_PASSWORD}
  310. volumes:
  311. - ./backup/backup.sh:/backup.sh:ro
  312. - supabase_backups:/backups
  313. entrypoint: ["/bin/sh", "/backup.sh"]
  314. networks:
  315. - internal
  316. profiles:
  317. - backup
  318. volumes:
  319. supabase_db:
  320. supabase_backups:
  321. caddy_data:
  322. caddy_config:
  323. networks:
  324. internal:
  325. driver: bridge