diff --git a/blueprints/supabase/docker-compose.yml b/blueprints/supabase/docker-compose.yml index 506ddc90..3e03fb2c 100644 --- a/blueprints/supabase/docker-compose.yml +++ b/blueprints/supabase/docker-compose.yml @@ -1,89 +1,107 @@ # Usage -# Start: docker compose up -# With helpers: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up +# Start: docker compose up -d # Stop: docker compose down -# Destroy: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans -# Reset everything: ./reset.sh +# Reset everything: ./reset.sh +# +# Dokploy: Kong is exposed on port 8000 for domain routing. Storage uses local file +# backend by default; edit the storage service env in this file to switch to S3/MinIO. name: supabase services: studio: - container_name: ${CONTAINER_PREFIX}-studio - image: supabase/studio:2025.04.21-sha-173cc56 + image: supabase/studio:2026.04.27-sha-5f60601 restart: unless-stopped healthcheck: test: [ - "CMD", - "node", - "-e", - "fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})" + "CMD-SHELL", + "node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\"" ] timeout: 10s interval: 5s - retries: 3 + retries: 5 + start_period: 30s depends_on: analytics: condition: service_healthy environment: + HOSTNAME: "0.0.0.0" + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} + PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public} + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} OPENAI_API_KEY: ${OPENAI_API_KEY:-} - SUPABASE_URL: ${SUPABASE_PUBLIC_URL} + SUPABASE_URL: http://kong:8000 SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} AUTH_JWT_SECRET: ${JWT_SECRET} - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_URL: http://analytics:4000 - NEXT_PUBLIC_ENABLE_LOGS: true - # Comment to use Big Query backend for analytics + NEXT_PUBLIC_ENABLE_LOGS: "true" NEXT_ANALYTICS_BACKEND_PROVIDER: postgres - # Uncomment to use Big Query backend for analytics - # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + SNIPPETS_MANAGEMENT_FOLDER: /app/snippets + EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions + volumes: + - ../files/volumes/snippets:/app/snippets:Z + - ../files/volumes/functions:/app/edge-functions:Z kong: - container_name: ${CONTAINER_PREFIX}-kong - image: kong:2.8.1 + image: kong/kong:3.9.1 restart: unless-stopped - # ports: - # - ${KONG_HTTP_PORT}:8000/tcp - # - ${KONG_HTTPS_PORT}:8443/tcp + healthcheck: + test: ["CMD", "kong", "health"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 30s + depends_on: + studio: + condition: service_healthy expose: - 8000 - 8443 volumes: - # https://github.com/supabase/supabase/issues/12661 - ../files/volumes/api/kong.yml:/home/kong/temp.yml:ro,z - depends_on: - analytics: - condition: service_healthy + - ../files/volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z environment: KONG_DATABASE: "off" - KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml - # https://github.com/supabase/cli/issues/14 + KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml KONG_DNS_ORDER: LAST,A,CNAME - KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_DNS_NOT_FOUND_TTL: 1 + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + KONG_PROXY_ACCESS_LOG: /dev/stdout combined SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-} + SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} + ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-} + SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-} DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} - CONTAINER_PREFIX: ${CONTAINER_PREFIX} - # https://unix.stackexchange.com/a/294837 - entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + entrypoint: ["/bin/bash", "/home/kong/kong-entrypoint.sh"] auth: - container_name: ${CONTAINER_PREFIX}-auth - image: supabase/gotrue:v2.171.0 + image: supabase/gotrue:v2.186.0 restart: unless-stopped healthcheck: test: @@ -100,12 +118,8 @@ services: retries: 3 depends_on: db: - # Disable this if you are using an external Postgres database - condition: service_healthy - analytics: condition: service_healthy environment: - # the next line seems required if you want to be able to send mails from the supabase GUI GOTRUE_MAILER_EXTERNAL_HOSTS: kong,${SUPABASE_HOST} GOTRUE_API_HOST: 0.0.0.0 GOTRUE_API_PORT: 9999 @@ -123,16 +137,12 @@ services: GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated GOTRUE_JWT_EXP: ${JWT_EXPIRY} GOTRUE_JWT_SECRET: ${JWT_SECRET} + GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]} GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} - # Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile. - # GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: true - - # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true - # GOTRUE_SMTP_MAX_FREQUENCY: 1s GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} GOTRUE_SMTP_HOST: ${SMTP_HOST} GOTRUE_SMTP_PORT: ${SMTP_PORT} @@ -146,39 +156,18 @@ services: GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} - # Uncomment to enable custom access token hook. Please see: https://supabase.com/docs/guides/auth/auth-hooks for full list of hooks and additional details about custom_access_token_hook - - # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true" - # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook" - # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: "" - - # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: "true" - # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/mfa_verification_attempt" - - # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: "true" - # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/password_verification_attempt" - - # GOTRUE_HOOK_SEND_SMS_ENABLED: "false" - # GOTRUE_HOOK_SEND_SMS_URI: "pg-functions://postgres/public/custom_access_token_hook" - # GOTRUE_HOOK_SEND_SMS_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" - - # GOTRUE_HOOK_SEND_EMAIL_ENABLED: "false" - # GOTRUE_HOOK_SEND_EMAIL_URI: "http://host.docker.internal:54321/functions/v1/email_sender" - # GOTRUE_HOOK_SEND_EMAIL_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n" rest: - container_name: ${CONTAINER_PREFIX}-rest - image: postgrest/postgrest:v12.2.11 + image: postgrest/postgrest:v14.8 restart: unless-stopped depends_on: db: - # Disable this if you are using an external Postgres database - condition: service_healthy - analytics: condition: service_healthy environment: PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} + PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public} PGRST_DB_ANON_ROLE: anon PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" @@ -190,32 +179,21 @@ services: ] realtime: - # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain - container_name: realtime-dev.${CONTAINER_PREFIX}-realtime - image: supabase/realtime:v2.34.47 + image: supabase/realtime:v2.76.5 restart: unless-stopped depends_on: db: - # Disable this if you are using an external Postgres database - condition: service_healthy - analytics: condition: service_healthy healthcheck: test: [ - "CMD", - "curl", - "-sSfL", - "--head", - "-o", - "/dev/null", - "-H", - "Authorization: Bearer ${ANON_KEY}", - "http://localhost:4000/api/tenants/realtime-dev/health" + "CMD-SHELL", + "curl -sSfL --head -o /dev/null -H \"Authorization: Bearer ${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health" ] timeout: 5s - interval: 5s + interval: 30s retries: 3 + start_period: 10s environment: PORT: 4000 DB_HOST: ${POSTGRES_HOST} @@ -227,20 +205,25 @@ services: DB_ENC_KEY: supabaserealtime API_JWT_SECRET: ${JWT_SECRET} SECRET_KEY_BASE: ${SECRET_KEY_BASE} + METRICS_JWT_SECRET: ${JWT_SECRET} ERL_AFLAGS: -proto_dist inet_tcp DNS_NODES: "''" RLIMIT_NOFILE: "10000" APP_NAME: realtime - SEED_SELF_HOST: true - RUN_JANITOR: true + SEED_SELF_HOST: "true" + RUN_JANITOR: "true" + DISABLE_HEALTHCHECK_LOGGING: "true" - # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: - container_name: ${CONTAINER_PREFIX}-storage - image: supabase/storage-api:v1.22.7 + image: supabase/storage-api:v1.48.26 restart: unless-stopped - volumes: - - ../files/volumes/storage:/var/lib/storage:z + depends_on: + db: + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started healthcheck: test: [ @@ -254,33 +237,30 @@ services: timeout: 5s interval: 5s retries: 3 - depends_on: - db: - # Disable this if you are using an external Postgres database - condition: service_healthy - rest: - condition: service_started - imgproxy: - condition: service_started + start_period: 10s + volumes: + - ../files/volumes/storage:/var/lib/storage:z environment: ANON_KEY: ${ANON_KEY} SERVICE_KEY: ${SERVICE_ROLE_KEY} POSTGREST_URL: http://rest:3000 - PGRST_JWT_SECRET: ${JWT_SECRET} + AUTH_JWT_SECRET: ${JWT_SECRET} DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + STORAGE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} + REQUEST_ALLOW_X_FORWARDED_PATH: "true" FILE_SIZE_LIMIT: 52428800 STORAGE_BACKEND: file + GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET} FILE_STORAGE_BACKEND_PATH: /var/lib/storage - TENANT_ID: stub - # TODO: https://github.com/supabase/storage-api/issues/55 - REGION: stub - GLOBAL_S3_BUCKET: stub + TENANT_ID: ${STORAGE_TENANT_ID} + REGION: ${REGION} ENABLE_IMAGE_TRANSFORMATION: "true" IMGPROXY_URL: http://imgproxy:5001 + S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID} + S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET} imgproxy: - container_name: ${CONTAINER_PREFIX}-imgproxy - image: darthsim/imgproxy:v3.8.0 + image: darthsim/imgproxy:v3.30.1 restart: unless-stopped volumes: - ../files/volumes/storage:/var/lib/storage:z @@ -298,17 +278,14 @@ services: IMGPROXY_BIND: ":5001" IMGPROXY_LOCAL_FILESYSTEM_ROOT: / IMGPROXY_USE_ETAG: "true" - IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP} + IMGPROXY_MAX_SRC_RESOLUTION: 16.8 meta: - container_name: ${CONTAINER_PREFIX}-meta - image: supabase/postgres-meta:v0.88.9 + image: supabase/postgres-meta:v0.96.3 restart: unless-stopped depends_on: db: - # Disable this if you are using an external Postgres database - condition: service_healthy - analytics: condition: service_healthy environment: PG_META_PORT: 8080 @@ -317,23 +294,26 @@ services: PG_META_DB_NAME: ${POSTGRES_DB} PG_META_DB_USER: supabase_admin PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + CRYPTO_KEY: ${PG_META_CRYPTO_KEY} functions: - container_name: ${CONTAINER_PREFIX}-edge-functions - image: supabase/edge-runtime:v1.67.4 + image: supabase/edge-runtime:v1.71.2 restart: unless-stopped volumes: - ../files/volumes/functions:/home/deno/functions:Z + - deno-cache:/root/.cache/deno depends_on: - analytics: + kong: condition: service_healthy environment: JWT_SECRET: ${JWT_SECRET} SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL} SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_PUBLISHABLE_KEYS: "{\"default\":\"${SUPABASE_PUBLISHABLE_KEY:-}\"}" + SUPABASE_SECRET_KEYS: "{\"default\":\"${SUPABASE_SECRET_KEY:-}\"}" SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" command: [ @@ -343,32 +323,23 @@ services: ] analytics: - container_name: ${CONTAINER_PREFIX}-analytics - image: supabase/logflare:1.12.0 + image: supabase/logflare:1.36.1 restart: unless-stopped - # ports: - # - 4000:4000 expose: - 4000 - # Uncomment to use Big Query backend for analytics - # volumes: - # - type: bind - # source: ${PWD}/gcloud.json - # target: /opt/app/rel/logflare/bin/gcloud.json - # read_only: true + # First boot: Logflare seeds _analytics in Postgres; allow extra time before marking unhealthy. healthcheck: test: [ - "CMD", - "curl", - "http://localhost:4000/health" + "CMD-SHELL", + "curl -sSfL -o /dev/null http://localhost:4000/health" ] - timeout: 5s + timeout: 10s interval: 5s - retries: 10 + retries: 15 + start_period: 90s depends_on: db: - # Disable this if you are using an external Postgres database condition: service_healthy environment: LOGFLARE_NODE_HOST: 127.0.0.1 @@ -378,55 +349,42 @@ services: DB_PORT: ${POSTGRES_PORT} DB_PASSWORD: ${POSTGRES_PASSWORD} DB_SCHEMA: _analytics - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} - LOGFLARE_SINGLE_TENANT: true - LOGFLARE_SUPABASE_MODE: true + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} + LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + LOGFLARE_SINGLE_TENANT: "true" + LOGFLARE_SUPABASE_MODE: "true" LOGFLARE_MIN_CLUSTER_SIZE: 1 - - # Comment variables to use Big Query backend for analytics POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase POSTGRES_BACKEND_SCHEMA: _analytics LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true - # Uncomment to use Big Query backend for analytics - # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} - # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} - # Comment out everything below this point if you are using an external Postgres database db: - container_name: ${CONTAINER_PREFIX}-db - image: supabase/postgres:15.8.1.060 + image: supabase/postgres:15.8.1.085 restart: unless-stopped volumes: - ../files/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z - # Must be superuser to create event trigger - ../files/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z - # Must be superuser to alter reserved role - ../files/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z - # Initialize the database settings with JWT_SECRET and JWT_EXP - ../files/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z - # PGDATA directory is persisted between restarts - ../files/volumes/db/data:/var/lib/postgresql/data:Z - # Changes required for internal supabase data such as _analytics - ../files/volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z - # Changes required for Analytics support - ../files/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z - # Changes required for Pooler support - ../files/volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z - # Use named volume to persist pgsodium decryption key between restarts - db-config:/etc/postgresql-custom healthcheck: test: [ - "CMD", - "pg_isready", - "-U", - "postgres", - "-h", - "localhost" + "CMD", + "pg_isready", + "-U", + "postgres", + "-h", + "localhost" ] interval: 5s timeout: 5s - retries: 10 + retries: 15 + start_period: 60s depends_on: vector: condition: service_healthy @@ -446,12 +404,11 @@ services: "-c", "config_file=/etc/postgresql/postgresql.conf", "-c", - "log_min_messages=fatal" # prevents Realtime polling queries from appearing in logs + "log_min_messages=fatal" ] vector: - container_name: ${CONTAINER_PREFIX}-vector - image: timberio/vector:0.28.1-alpine + image: timberio/vector:0.53.0-alpine restart: unless-stopped volumes: - ../files/volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z @@ -468,9 +425,10 @@ services: ] timeout: 5s interval: 5s - retries: 3 + retries: 5 + start_period: 10s environment: - LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} command: [ "--config", @@ -479,14 +437,12 @@ services: security_opt: - "label=disable" - # Update the DATABASE_URL if you are using an external Postgres database supavisor: - container_name: ${CONTAINER_PREFIX}-pooler - image: supabase/supavisor:2.5.1 + image: supabase/supavisor:2.7.4 restart: unless-stopped - ports: # expose supavisor to the host to enable db pooler connection - - ${POSTGRES_PORT}:5432 - - ${POOLER_PROXY_PORT_TRANSACTION}:6543 + expose: + - "5432" + - "6543" volumes: - ../files/volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z healthcheck: @@ -502,19 +458,19 @@ services: ] interval: 10s timeout: 5s - retries: 5 + retries: 10 + start_period: 30s depends_on: db: condition: service_healthy - analytics: - condition: service_healthy environment: PORT: 4000 POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/_supabase - CLUSTER_POSTGRES: true + DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase + CLUSTER_POSTGRES: "true" SECRET_KEY_BASE: ${SECRET_KEY_BASE} VAULT_ENC_KEY: ${VAULT_ENC_KEY} API_JWT_SECRET: ${JWT_SECRET} @@ -525,6 +481,7 @@ services: POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE} POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN} POOLER_POOL_MODE: transaction + DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE} command: [ "/bin/sh", @@ -533,4 +490,5 @@ services: ] volumes: - db-config: \ No newline at end of file + db-config: + deno-cache: diff --git a/blueprints/supabase/template.toml b/blueprints/supabase/template.toml index f70c93cc..0e76b7dd 100644 --- a/blueprints/supabase/template.toml +++ b/blueprints/supabase/template.toml @@ -2,11 +2,15 @@ main_domain = "${domain}" postgres_password = "${password:32}" dashboard_password = "${password:32}" -logflare_api_key = "${password:32}" +logflare_public_access_token = "${password:32}" +logflare_private_access_token = "${password:32}" +pg_meta_crypto_key = "${password:32}" +s3_protocol_access_key_id = "${password:24}" +s3_protocol_access_key_secret = "${password:48}" secret_key_base = "${password:64}" vault_enc_key = "${password:32}" jwt_secret = "${password:32}" -container_name_prefix = "${APP_NAME}-supabase" +pooler_tenant_id = "${uuid}" anon_key_payload = """{ "role": "anon", "iss": "supabase", @@ -34,12 +38,7 @@ env = [ '# - ADDITIONAL_REDIRECT_URLS, SITE_URL should point to application using supabase for authentication', '# They are used for redirecting after login/signup and gotrue will check them before sending emails', '# - POSTGRES_PORT, POOLER_PROXY_PORT_TRANSACTION should be changed if you are already running other instances of supabase', -'#', -'# Supabase uses container names in part of its configuration so it is important to keep them', -'# This template generates a random prefix for the container names to avoid conflicts', -'# If you change it you will need to update routes in the vector.yml file in advanced->mounts section', '############', -'CONTAINER_PREFIX=${container_name_prefix}', '', '############', '# Secrets', @@ -59,6 +58,9 @@ env = [ 'DASHBOARD_PASSWORD=${dashboard_password}', 'SECRET_KEY_BASE=${secret_key_base}', 'VAULT_ENC_KEY=${vault_enc_key}', +'PG_META_CRYPTO_KEY=${pg_meta_crypto_key}', +'LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}', +'LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}', '', '', '############', @@ -68,7 +70,7 @@ env = [ 'POSTGRES_HOST=db', 'POSTGRES_DB=postgres', 'POSTGRES_PORT=5432', -'# default user is postgres', +'POSTGRES_USER=postgres', '', '', '############', @@ -77,7 +79,8 @@ env = [ 'POOLER_PROXY_PORT_TRANSACTION=6543', 'POOLER_DEFAULT_POOL_SIZE=20', 'POOLER_MAX_CLIENT_CONN=100', -'POOLER_TENANT_ID=your-tenant-id', +'POOLER_TENANT_ID=${pooler_tenant_id}', +'POOLER_DB_POOL_SIZE=5', '', '', '############', @@ -94,6 +97,8 @@ env = [ '############', '', 'PGRST_DB_SCHEMAS=public,storage,graphql_public', +'PGRST_DB_MAX_ROWS=1000', +'PGRST_DB_EXTRA_SEARCH_PATH=public', '', '', '############', @@ -102,10 +107,11 @@ env = [ '', '## General', 'SITE_URL=http://localhost:3000', -'ADDITIONAL_REDIRECT_URLS=http://${main_domain}/*,http://localhost:3000/*', +'ADDITIONAL_REDIRECT_URLS=https://${main_domain}/*,http://localhost:3000/*', 'JWT_EXPIRY=3600', 'DISABLE_SIGNUP=false', -'API_EXTERNAL_URL=http://${main_domain}', +'API_EXTERNAL_URL=https://${main_domain}', +'JWT_KEYS=[]', '', '## Mailer Config', 'MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"', @@ -136,12 +142,19 @@ env = [ 'STUDIO_DEFAULT_ORGANIZATION=Default Organization', 'STUDIO_DEFAULT_PROJECT=Default Project', '', -'STUDIO_PORT=3000', -'# replace if you intend to use Studio outside of localhost', -'SUPABASE_PUBLIC_URL=http://${main_domain}', +'SUPABASE_PUBLIC_URL=https://${main_domain}', '', -'# Enable webp support', -'IMGPROXY_ENABLE_WEBP_DETECTION=true', +'############', +'# Storage - local file backend (default). Edit docker-compose.yml storage env for S3/MinIO.', +'############', +'', +'GLOBAL_S3_BUCKET=stub', +'REGION=stub', +'STORAGE_TENANT_ID=stub', +'S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}', +'S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}', +'', +'IMGPROXY_AUTO_WEBP=true', '', '# Add your OpenAI API key to enable SQL Editor Assistant', 'OPENAI_API_KEY=', @@ -156,14 +169,8 @@ env = [ '', '############', '# Logs - Configuration for Logflare', -'# Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction', '############', '', -'LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key', -'', -'# Change vector.toml sinks to reflect this change', -'LOGFLARE_API_KEY=${logflare_api_key}', -'', '# Docker socket location - this value will differ depending on your OS', 'DOCKER_SOCKET_LOCATION=/var/run/docker.sock', '', @@ -171,6 +178,40 @@ env = [ 'GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID', 'GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER'] +[[config.mounts]] +filePath = "/volumes/api/kong-entrypoint.sh" +content = """#!/bin/bash +# Custom entrypoint for Kong that builds Lua expressions for request-transformer +# and performs environment variable substitution in the declarative config. + +if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)" + export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)" +else + export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)" + export LUA_RT_WS_EXPR="\$(query_params.apikey)" +fi + +awk '{ + result = "" + rest = $0 + while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) { + varname = substr(rest, RSTART + 1, RLENGTH - 1) + if (varname in ENVIRON) { + result = result substr(rest, 1, RSTART - 1) ENVIRON[varname] + } else { + result = result substr(rest, 1, RSTART + RLENGTH - 1) + } + rest = substr(rest, RSTART + RLENGTH) + } + print result rest +}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG" + +sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG" + +exec /entrypoint.sh kong docker-start +""" + [[config.mounts]] filePath = "/volumes/api/kong.yml" content = """_format_version: '2.1' @@ -184,9 +225,11 @@ consumers: - username: anon keyauth_credentials: - key: $SUPABASE_ANON_KEY + - key: $SUPABASE_PUBLISHABLE_KEY - username: service_role keyauth_credentials: - key: $SUPABASE_SERVICE_KEY + - key: $SUPABASE_SECRET_KEY ### ### Access Control List @@ -202,8 +245,8 @@ acls: ### basicauth_credentials: - consumer: DASHBOARD - username: $DASHBOARD_USERNAME - password: $DASHBOARD_PASSWORD + username: '$DASHBOARD_USERNAME' + password: '$DASHBOARD_PASSWORD' ### ### API Routes @@ -237,6 +280,33 @@ services: - /auth/v1/authorize plugins: - name: cors + - name: auth-v1-open-jwks + url: http://auth:9999/.well-known/jwks.json + routes: + - name: auth-v1-open-jwks + strip_path: true + paths: + - /auth/v1/.well-known/jwks.json + plugins: + - name: cors + - name: auth-v1-open-sso-acs + url: http://auth:9999/sso/saml/acs + routes: + - name: auth-v1-open-sso-acs + strip_path: true + paths: + - /sso/saml/acs + plugins: + - name: cors + - name: auth-v1-open-sso-metadata + url: http://auth:9999/sso/saml/metadata + routes: + - name: auth-v1-open-sso-metadata + strip_path: true + paths: + - /sso/saml/metadata + plugins: + - name: cors ## Secure Auth routes - name: auth-v1 @@ -252,6 +322,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -272,7 +350,15 @@ services: - name: cors - name: key-auth config: - hide_credentials: true + hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -298,7 +384,11 @@ services: config: add: headers: - - Content-Profile:graphql_public + - "Content-Profile: graphql_public" + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true @@ -309,7 +399,7 @@ services: ## Secure Realtime routes - name: realtime-v1-ws _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' - url: http://realtime-dev.${CONTAINER_PREFIX}-realtime:4000/socket + url: http://realtime:4000/socket protocol: ws routes: - name: realtime-v1-ws @@ -321,6 +411,14 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "x-api-key:$LUA_RT_WS_EXPR" + replace: + querystring: + - "apikey:$LUA_RT_WS_EXPR" - name: acl config: hide_groups_header: true @@ -328,8 +426,8 @@ services: - admin - anon - name: realtime-v1-rest - _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' - url: http://realtime-dev.${CONTAINER_PREFIX}-realtime:4000/api + _comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*' + url: http://realtime:4000/api protocol: http routes: - name: realtime-v1-rest @@ -341,13 +439,22 @@ services: - name: key-auth config: hide_credentials: false + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" - name: acl config: hide_groups_header: true allow: - admin - anon - ## Storage routes: the storage server manages its own auth + + ## Storage routes - name: storage-v1 _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' url: http://storage:5000/ @@ -358,11 +465,28 @@ services: - /storage/v1/ plugins: - name: cors + - name: request-transformer + config: + add: + headers: + - "Authorization: $LUA_AUTH_EXPR" + replace: + headers: + - "Authorization: $LUA_AUTH_EXPR" + - name: post-function + config: + access: + - | + local auth = kong.request.get_header("authorization") + if auth == nil or auth == "" or auth:find("^%s*$") then + kong.service.request.clear_header("authorization") + end ## Edge Functions routes - name: functions-v1 _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' url: http://functions:9000/ + read_timeout: 150000 routes: - name: functions-v1-all strip_path: true @@ -371,15 +495,16 @@ services: plugins: - name: cors - ## Analytics routes - - name: analytics-v1 - _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' - url: http://analytics:4000/ + ## OAuth 2.0 Authorization Server Metadata + - name: well-known-oauth + url: http://auth:9999/.well-known/oauth-authorization-server routes: - - name: analytics-v1-all + - name: well-known-oauth strip_path: true paths: - - /analytics/v1/ + - /.well-known/oauth-authorization-server + plugins: + - name: cors ## Secure Database routes - name: meta @@ -400,6 +525,32 @@ services: allow: - admin + - name: mcp-blocker + url: http://studio:3000/api/mcp + routes: + - name: mcp-blocker-route + strip_path: true + paths: + - /api/mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + + - name: mcp + url: http://studio:3000/api/mcp + routes: + - name: mcp + strip_path: true + paths: + - /mcp + plugins: + - name: request-termination + config: + status_code: 403 + message: "Access is forbidden." + ## Protected Dashboard - catch all remaining routes - name: dashboard _comment: 'Studio: /* -> http://studio:3000/*' @@ -416,6 +567,10 @@ services: hide_credentials: true """ +[[config.mounts]] +filePath = "/volumes/snippets/.gitkeep" +content = "" + [[config.mounts]] filePath = "/volumes/db/init/data.sql" content = "" @@ -717,14 +872,25 @@ serve(async () => { [[config.mounts]] filePath = "/volumes/functions/main/index.ts" -content = """import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' +content = """import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' console.log('main function started') const JWT_SECRET = Deno.env.get('JWT_SECRET') +const SUPABASE_URL = Deno.env.get('SUPABASE_URL') const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' +let SUPABASE_JWT_KEYS: ReturnType | null = null +if (SUPABASE_URL) { + try { + SUPABASE_JWT_KEYS = jose.createRemoteJWKSet( + new URL('/auth/v1/.well-known/jwks.json', SUPABASE_URL) + ) + } catch (e) { + console.error('Failed to fetch JWKS from SUPABASE_URL:', e) + } +} + function getAuthToken(req: Request) { const authHeader = req.headers.get('authorization') if (!authHeader) { @@ -737,25 +903,53 @@ function getAuthToken(req: Request) { return token } -async function verifyJWT(jwt: string): Promise { +async function isValidLegacyJWT(jwt: string): Promise { + if (!JWT_SECRET) { + console.error('JWT_SECRET not available for HS256 token verification') + return false + } const encoder = new TextEncoder() const secretKey = encoder.encode(JWT_SECRET) try { await jose.jwtVerify(jwt, secretKey) - } catch (err) { - console.error(err) + } catch (e) { + console.error('Symmetric Legacy JWT verification error', e) return false } return true } -serve(async (req: Request) => { +async function isValidJWT(jwt: string): Promise { + if (!SUPABASE_JWT_KEYS) { + console.error('JWKS not available for ES256/RS256 token verification') + return false + } + try { + await jose.jwtVerify(jwt, SUPABASE_JWT_KEYS) + } catch (e) { + console.error('Asymmetric JWT verification error', e) + return false + } + return true +} + +async function isValidHybridJWT(jwt: string): Promise { + const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt) + if (jwtAlgorithm === 'HS256') { + return await isValidLegacyJWT(jwt) + } + if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') { + return await isValidJWT(jwt) + } + return false +} + +Deno.serve(async (req: Request) => { if (req.method !== 'OPTIONS' && VERIFY_JWT) { try { const token = getAuthToken(req) - const isValidJWT = await verifyJWT(token) - - if (!isValidJWT) { + const isValid = await isValidHybridJWT(token) + if (!isValid) { return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { status: 401, headers: { 'Content-Type': 'application/json' }, @@ -822,8 +1016,6 @@ content = """api: sources: docker_host: type: docker_logs - exclude_containers: - - ${container_name_prefix}-vector transforms: project_logs: @@ -833,7 +1025,15 @@ transforms: source: |- .project = "default" .event_message = del(.message) - .appname = replace!(del(.container_name), "${container_name_prefix}", "supabase") + compose_service, label_err = get(.label, ["com.docker.compose.service"]) + if label_err != null || compose_service == null { + abort + } + compose_service = to_string!(compose_service) + if compose_service == "vector" { + abort + } + .appname = "supabase-" + compose_service del(.container_created_at) del(.container_id) del(.source_type) @@ -847,12 +1047,12 @@ transforms: inputs: - project_logs route: - kong: '.appname == "supabase-kong"' + kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"' auth: '.appname == "supabase-auth"' rest: '.appname == "supabase-rest"' - realtime: '.appname == "realtime-dev.${CONTAINER_PREFIX}-realtime"' + realtime: '.appname == "supabase-realtime"' storage: '.appname == "supabase-storage"' - functions: '.appname == "supabase-edge-functions"' + functions: '.appname == "supabase-functions"' db: '.appname == "supabase-db"' # Ignores non nginx errors since they are related with kong booting up kong_logs: @@ -866,15 +1066,17 @@ transforms: .metadata.request.headers.referer = req.referer .metadata.request.headers.user_agent = req.agent .metadata.request.headers.cf_connecting_ip = req.client - .metadata.request.method = req.method - .metadata.request.path = req.path - .metadata.request.protocol = req.protocol .metadata.response.status_code = req.status + url, split_err = split(req.request, " ") + if split_err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } } if err != null { abort } - # Ignores non nginx errors since they are related with kong booting up kong_err: type: remap inputs: @@ -918,14 +1120,18 @@ transforms: parsed, err = parse_regex(.event_message, r'^(?P