first commit
This commit is contained in:
366
.env.example
Normal file
366
.env.example
Normal file
@@ -0,0 +1,366 @@
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Native docker compose COMPOSE_FILE: colon-separated list, base file first.
|
||||
# Manage with: ./run.sh config add|remove <name>
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
############
|
||||
COMPOSE_FILE=docker-compose.yml
|
||||
|
||||
|
||||
############
|
||||
# Secrets
|
||||
#
|
||||
# YOU MUST CHANGE ALL THE DEFAULT VALUES BELOW BEFORE STARTING
|
||||
# THE CONTAINERS FOR THE FIRST TIME!
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase
|
||||
#
|
||||
# To generate secrets and API keys:
|
||||
# 1. sh utils/generate-keys.sh
|
||||
# 2. sh utils/add-new-auth-keys.sh
|
||||
#
|
||||
############
|
||||
|
||||
# Postgres
|
||||
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
|
||||
# Asymmetric key pair (ES256) and opaque API keys
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys
|
||||
#
|
||||
# To generate:
|
||||
# sh ./utils/add-new-auth-keys.sh
|
||||
#
|
||||
# Opaque API key for client-side use (anon role).
|
||||
SUPABASE_PUBLISHABLE_KEY=
|
||||
# Opaque API key for server-side use (service_role). Never expose in client code.
|
||||
SUPABASE_SECRET_KEY=
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric).
|
||||
# Used by Auth.
|
||||
JWT_KEYS=
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# Used by PostgREST, Realtime, Storage to verify tokens.
|
||||
JWT_JWKS=
|
||||
|
||||
# Access to Dashboard
|
||||
DASHBOARD_USERNAME=supabase
|
||||
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated
|
||||
|
||||
# Used by Realtime and Supavisor
|
||||
SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
|
||||
|
||||
# Used by Supavisor
|
||||
VAULT_ENC_KEY=your-32-character-encryption-key
|
||||
|
||||
# Used by Studio to access Postgres via postgres-meta
|
||||
PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min
|
||||
|
||||
# Analytics - API tokens for log ingestion/querying, and for management
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
# Used in the docker-compose.logs.yml override.
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-public
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-private
|
||||
|
||||
# Access to Storage via S3 protocol endpoint (see below)
|
||||
S3_PROTOCOL_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||
|
||||
|
||||
############
|
||||
# URLs - Configure hostnames below to reflect your actual domain name
|
||||
############
|
||||
|
||||
# Access to Dashboard and REST API
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
|
||||
# Full external URL of the Auth service, used to construct OAuth callbacks,
|
||||
# SAML endpoints, and email links
|
||||
API_EXTERNAL_URL=http://localhost:8000
|
||||
|
||||
# See also the Auth section below for Site URL and Redirect URLs configuration
|
||||
|
||||
|
||||
############
|
||||
# Database - Postgres configuration
|
||||
############
|
||||
|
||||
# Using default user (postgres)
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
|
||||
# Default configuration includes Supavisor exposing POSTGRES_PORT
|
||||
# Postgres uses POSTGRES_PORT inside the container
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres-through-supavisor
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
|
||||
############
|
||||
# Supavisor - Database pooler
|
||||
############
|
||||
|
||||
# Supavisor exposes POSTGRES_PORT and POOLER_PROXY_PORT_TRANSACTION,
|
||||
# POSTGRES_PORT is used for session mode pooling
|
||||
#
|
||||
# Port to use for transaction mode pooling connections
|
||||
POOLER_PROXY_PORT_TRANSACTION=6543
|
||||
|
||||
# Maximum number of PostgreSQL connections Supavisor opens per pool
|
||||
POOLER_DEFAULT_POOL_SIZE=20
|
||||
|
||||
# Maximum number of client connections Supavisor accepts per pool
|
||||
POOLER_MAX_CLIENT_CONN=100
|
||||
|
||||
# Unique Supavisor tenant identifier
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres
|
||||
POOLER_TENANT_ID=your-tenant-id
|
||||
|
||||
# Pool size for internal metadata storage used by Supavisor
|
||||
# This is separate from client connections and used only by Supavisor itself
|
||||
POOLER_DB_POOL_SIZE=5
|
||||
|
||||
|
||||
############
|
||||
# Studio - Configuration for the Dashboard
|
||||
############
|
||||
|
||||
STUDIO_DEFAULT_ORGANIZATION=Default Organization
|
||||
STUDIO_DEFAULT_PROJECT=Default Project
|
||||
|
||||
# Add your OpenAI API key to enable AI Assistant
|
||||
OPENAI_API_KEY=sk-proj-xxxxxxxx
|
||||
|
||||
|
||||
############
|
||||
# Auth - Configuration for the authentication server
|
||||
############
|
||||
|
||||
## General settings
|
||||
|
||||
# Equivalent to "Site URL" and "Redirect URLs" platform configuration options
|
||||
# Documentation: https://supabase.com/docs/guides/auth/redirect-urls
|
||||
SITE_URL=http://localhost:3000
|
||||
ADDITIONAL_REDIRECT_URLS=
|
||||
|
||||
JWT_EXPIRY=3600
|
||||
DISABLE_SIGNUP=false
|
||||
|
||||
## Mailer Config
|
||||
MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
|
||||
MAILER_URLPATHS_INVITE="/auth/v1/verify"
|
||||
MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
|
||||
MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
|
||||
|
||||
## Email auth
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=supabase-mail
|
||||
SMTP_PORT=2500
|
||||
SMTP_USER=fake_mail_user
|
||||
SMTP_PASS=fake_mail_password
|
||||
SMTP_SENDER_NAME=fake_sender
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
## Phone auth
|
||||
ENABLE_PHONE_SIGNUP=true
|
||||
ENABLE_PHONE_AUTOCONFIRM=true
|
||||
|
||||
## OAuth / Social login providers
|
||||
|
||||
# Uncomment and fill in the providers you want to enable.
|
||||
# You must ALSO uncomment the matching GOTRUE_EXTERNAL_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-oauth
|
||||
# GOOGLE_ENABLED=false
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_SECRET=
|
||||
|
||||
# GITHUB_ENABLED=false
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_SECRET=
|
||||
|
||||
# AZURE_ENABLED=false
|
||||
# AZURE_CLIENT_ID=
|
||||
# AZURE_SECRET=
|
||||
|
||||
# Phone / SMS provider configuration
|
||||
# Uncomment to configure SMS delivery for phone auth and phone MFA.
|
||||
# You must ALSO uncomment the matching GOTRUE_SMS_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-phone-mfa
|
||||
# SMS_PROVIDER=twilio
|
||||
# SMS_OTP_EXP=60
|
||||
# SMS_OTP_LENGTH=6
|
||||
# SMS_MAX_FREQUENCY=60s
|
||||
# SMS_TEMPLATE=Your code is {{ .Code }}
|
||||
|
||||
# SMS_TWILIO_ACCOUNT_SID=
|
||||
# SMS_TWILIO_AUTH_TOKEN=
|
||||
# SMS_TWILIO_MESSAGE_SERVICE_SID=
|
||||
|
||||
# Test OTP: map phone numbers to fixed OTP codes for development
|
||||
# Format: phone1:code1,phone2:code2
|
||||
# SMS_TEST_OTP=
|
||||
|
||||
# Multi-factor authentication (MFA)
|
||||
# Uncomment to change MFA defaults.
|
||||
# You must ALSO uncomment the matching GOTRUE_MFA_* lines in docker-compose.yml
|
||||
|
||||
# App Authenticator (TOTP) - enabled by default
|
||||
# MFA_TOTP_ENROLL_ENABLED=true
|
||||
# MFA_TOTP_VERIFY_ENABLED=true
|
||||
|
||||
# Phone MFA - disabled by default (opt-in)
|
||||
# MFA_PHONE_ENROLL_ENABLED=false
|
||||
# MFA_PHONE_VERIFY_ENABLED=false
|
||||
|
||||
# Maximum MFA factors a user can enroll
|
||||
# MFA_MAX_ENROLLED_FACTORS=10
|
||||
|
||||
## SAML SSO
|
||||
|
||||
# You must ALSO uncomment the matching GOTRUE_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso
|
||||
|
||||
# SAML_ENABLED=true
|
||||
# SAML_PRIVATE_KEY=<your-base64-encoded-private-key>
|
||||
|
||||
# Optional: accept encrypted SAML assertions from IdPs (default: false)
|
||||
# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false
|
||||
|
||||
# Optional: how long relay state tokens remain valid (default: 2m0s)
|
||||
# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s
|
||||
|
||||
# Optional: override the SAML entity ID / ACS base URL
|
||||
# Defaults to API_EXTERNAL_URL if not set
|
||||
# SAML_EXTERNAL_URL=https://supabase.example.com:8000
|
||||
|
||||
# Optional: rate limit on the ACS endpoint (requests per second, default: 15)
|
||||
# SAML_RATE_LIMIT_ASSERTION=15
|
||||
|
||||
|
||||
############
|
||||
# Storage - Configuration for Storage
|
||||
############
|
||||
|
||||
# Check the S3_PROTOCOL_ACCESS_KEY_ID/SECRET above, and
|
||||
# refer to the documentation at:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-s3
|
||||
# to learn how to configure the S3 protocol endpoint
|
||||
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET=stub
|
||||
|
||||
# Used for S3 protocol endpoint configuration
|
||||
REGION=stub
|
||||
|
||||
# Used by MinIO when added via:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
MINIO_ROOT_USER=supa-storage
|
||||
MINIO_ROOT_PASSWORD=secret1234
|
||||
|
||||
# Equivalent to project_ref as described here:
|
||||
# https://supabase.com/docs/guides/storage/s3/authentication#session-token
|
||||
STORAGE_TENANT_ID=stub
|
||||
|
||||
|
||||
############
|
||||
# Functions - Configuration for Edge functions
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-functions
|
||||
|
||||
# NOTE: VERIFY_JWT applies to all functions
|
||||
FUNCTIONS_VERIFY_JWT=false
|
||||
|
||||
|
||||
############
|
||||
# API - Configuration for PostgREST
|
||||
############
|
||||
|
||||
# Postgres schemas exposed via the REST API
|
||||
PGRST_DB_SCHEMAS=public,storage,graphql_public
|
||||
|
||||
# Max number of rows returned by a request
|
||||
PGRST_DB_MAX_ROWS=1000
|
||||
|
||||
# Extra schemas added to the search_path of every request
|
||||
PGRST_DB_EXTRA_SEARCH_PATH=public
|
||||
|
||||
|
||||
############
|
||||
# Logs and Analytics
|
||||
############
|
||||
|
||||
## Vector log collection and routing
|
||||
|
||||
# Docker socket location - required for proper Vector operation
|
||||
DOCKER_SOCKET_LOCATION=/var/run/docker.sock
|
||||
# For Podman use the following:
|
||||
# DOCKER_SOCKET_LOCATION=/run/podman/podman.sock
|
||||
|
||||
## Analytics (Logflare)
|
||||
|
||||
# Check the LOGFLARE_* access token configuration _above_.
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
|
||||
# Google Cloud Project details
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/reference/self-hosting-analytics/introduction
|
||||
GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
|
||||
GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
|
||||
|
||||
|
||||
############
|
||||
# API gateway
|
||||
############
|
||||
|
||||
# Kong configuration variables
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# Used internally by the API gateway - DO NOT use in any client or server code.
|
||||
# Pre-signed ES256 JWT "API key" for anon role.
|
||||
ANON_KEY_ASYMMETRIC=
|
||||
# Pre-signed ES256 JWT "API key" for service_role.
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=
|
||||
|
||||
|
||||
############
|
||||
# imgproxy
|
||||
############
|
||||
|
||||
# Enable webp support
|
||||
IMGPROXY_AUTO_WEBP=true
|
||||
|
||||
|
||||
############
|
||||
# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https
|
||||
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
|
||||
# Domain name for the proxy (must point to your server)
|
||||
PROXY_DOMAIN=your-domain.example.com
|
||||
|
||||
# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN).
|
||||
# This should be a valid email, not a placehoder (otherwise Certbot may fail to start).
|
||||
CERTBOT_EMAIL=admin@example.com
|
||||
366
.env.old
Normal file
366
.env.old
Normal file
@@ -0,0 +1,366 @@
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Native docker compose COMPOSE_FILE: colon-separated list, base file first.
|
||||
# Manage with: ./run.sh config add|remove <name>
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
############
|
||||
COMPOSE_FILE=docker-compose.yml
|
||||
|
||||
|
||||
############
|
||||
# Secrets
|
||||
#
|
||||
# YOU MUST CHANGE ALL THE DEFAULT VALUES BELOW BEFORE STARTING
|
||||
# THE CONTAINERS FOR THE FIRST TIME!
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase
|
||||
#
|
||||
# To generate secrets and API keys:
|
||||
# 1. sh utils/generate-keys.sh
|
||||
# 2. sh utils/add-new-auth-keys.sh
|
||||
#
|
||||
############
|
||||
|
||||
# Postgres
|
||||
POSTGRES_PASSWORD=7da49c3b4a62d446430082bd0cb3c238
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET=gNAGgi6vYfyMqFMyHT5nbFow1MklGE57/fiap9pA
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzgwNjY1NDc0LCJleHAiOjE5MzgzNDU0NzR9.IdSVAHLcELmrQj19YU-7Tb_oI0BwQyADfRDu4qdW1sk
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3ODA2NjU0NzQsImV4cCI6MTkzODM0NTQ3NH0.jvJnzgYVbx1aQOhhs3nBL99LdfPTGoa2haDtpIYW-dU
|
||||
|
||||
# Asymmetric key pair (ES256) and opaque API keys
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys
|
||||
#
|
||||
# To generate:
|
||||
# sh ./utils/add-new-auth-keys.sh
|
||||
#
|
||||
# Opaque API key for client-side use (anon role).
|
||||
SUPABASE_PUBLISHABLE_KEY=sb_publishable_PNDlYaG13ZGdG67ZlpKlC3_bc8PpuRg
|
||||
# Opaque API key for server-side use (service_role). Never expose in client code.
|
||||
SUPABASE_SECRET_KEY=sb_secret_ePbz8YDnis8hpLJb4z7dMY_oRwPtoUh
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric).
|
||||
# Used by Auth.
|
||||
JWT_KEYS=[{"kty":"EC","kid":"ed13496a-53df-43e8-afb3-2016fe217508","use":"sig","key_ops":["sign","verify"],"alg":"ES256","ext":true,"crv":"P-256","x":"2eIZ3MgewDAJwxrmYJRXjOvZsf3VkWKz8THYYfyHlK8","y":"qad03ZZhMadwdlCkNgI8Lyatjh22EKvNVGsCPq84B18","d":"TPZ3pFwn0bmgKSo6T81PCuJm8BDcNOTqUXbKN6NVkQc"},{"kty":"oct","k":"Z05BR2dpNnZZZnlNcUZNeUhUNW5iRm93MU1rbEdFNTcvZmlhcDlwQQ","alg":"HS256"}]
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# Used by PostgREST, Realtime, Storage to verify tokens.
|
||||
JWT_JWKS={"keys":[{"kty":"EC","kid":"71dc613b-7ab3-493c-8df2-cd6c66026e70","use":"sig","key_ops":["verify"],"alg":"ES256","ext":true,"crv":"P-256","x":"TOy2AYvt1WuUNSrxy2eWZQmT5yMmDUKNv74Kz9Yt964","y":"sxNABQp1bA12RZjoVSvp4wWO3GTDDF_U54YrfPP3YlY"},{"kty":"oct","k":"QzFiRUpHNElqcE51dlIxODJZUDZFeFZjN1g0Y2dQU2dsOXF5UGlUQg","alg":"HS256"}]}
|
||||
|
||||
# Access to Dashboard
|
||||
DASHBOARD_USERNAME=supabase
|
||||
DASHBOARD_PASSWORD=909f5980a3542c84c6ae6bcb82fdf8a0
|
||||
|
||||
# Used by Realtime and Supavisor
|
||||
SECRET_KEY_BASE=cnrV6e3IdLYYQRMXW7ya+Q5nNAKj6sX23NDcWPQ1X8za/elrvWI/r96iIa/MRZgo
|
||||
|
||||
# Used by Supavisor
|
||||
VAULT_ENC_KEY=5a7707d9068ca3fc7473eee5ac372e35
|
||||
|
||||
# Used by Studio to access Postgres via postgres-meta
|
||||
PG_META_CRYPTO_KEY=Q7yvLaLiy3T9A97LBmnDUJMjfE3HTbei
|
||||
|
||||
# Analytics - API tokens for log ingestion/querying, and for management
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
# Used in the docker-compose.logs.yml override.
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN=dJ78MZD//eVLtP1y0YAA4p+vjTjEvukX
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN=3PK3vgvRoX5Np9CxBQ1aQgL7OEC+/SS+
|
||||
|
||||
# Access to Storage via S3 protocol endpoint (see below)
|
||||
S3_PROTOCOL_ACCESS_KEY_ID=7791a77abdfbd8dbbb4855ec341388e0
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET=46fbaccdd6188dec014200b89e43e5f1c98d95c3be40196defc504190d1d98c0
|
||||
|
||||
|
||||
############
|
||||
# URLs - Configure hostnames below to reflect your actual domain name
|
||||
############
|
||||
|
||||
# Access to Dashboard and REST API
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
|
||||
# Full external URL of the Auth service, used to construct OAuth callbacks,
|
||||
# SAML endpoints, and email links
|
||||
API_EXTERNAL_URL=http://localhost:8000
|
||||
|
||||
# See also the Auth section below for Site URL and Redirect URLs configuration
|
||||
|
||||
|
||||
############
|
||||
# Database - Postgres configuration
|
||||
############
|
||||
|
||||
# Using default user (postgres)
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
|
||||
# Default configuration includes Supavisor exposing POSTGRES_PORT
|
||||
# Postgres uses POSTGRES_PORT inside the container
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres-through-supavisor
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
|
||||
############
|
||||
# Supavisor - Database pooler
|
||||
############
|
||||
|
||||
# Supavisor exposes POSTGRES_PORT and POOLER_PROXY_PORT_TRANSACTION,
|
||||
# POSTGRES_PORT is used for session mode pooling
|
||||
#
|
||||
# Port to use for transaction mode pooling connections
|
||||
POOLER_PROXY_PORT_TRANSACTION=6543
|
||||
|
||||
# Maximum number of PostgreSQL connections Supavisor opens per pool
|
||||
POOLER_DEFAULT_POOL_SIZE=20
|
||||
|
||||
# Maximum number of client connections Supavisor accepts per pool
|
||||
POOLER_MAX_CLIENT_CONN=100
|
||||
|
||||
# Unique Supavisor tenant identifier
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#accessing-postgres
|
||||
POOLER_TENANT_ID=your-tenant-id
|
||||
|
||||
# Pool size for internal metadata storage used by Supavisor
|
||||
# This is separate from client connections and used only by Supavisor itself
|
||||
POOLER_DB_POOL_SIZE=5
|
||||
|
||||
|
||||
############
|
||||
# Studio - Configuration for the Dashboard
|
||||
############
|
||||
|
||||
STUDIO_DEFAULT_ORGANIZATION=Default Organization
|
||||
STUDIO_DEFAULT_PROJECT=Default Project
|
||||
|
||||
# Add your OpenAI API key to enable AI Assistant
|
||||
OPENAI_API_KEY=sk-proj-xxxxxxxx
|
||||
|
||||
|
||||
############
|
||||
# Auth - Configuration for the authentication server
|
||||
############
|
||||
|
||||
## General settings
|
||||
|
||||
# Equivalent to "Site URL" and "Redirect URLs" platform configuration options
|
||||
# Documentation: https://supabase.com/docs/guides/auth/redirect-urls
|
||||
SITE_URL=http://localhost:3000
|
||||
ADDITIONAL_REDIRECT_URLS=
|
||||
|
||||
JWT_EXPIRY=3600
|
||||
DISABLE_SIGNUP=false
|
||||
|
||||
## Mailer Config
|
||||
MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
|
||||
MAILER_URLPATHS_INVITE="/auth/v1/verify"
|
||||
MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
|
||||
MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
|
||||
|
||||
## Email auth
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=supabase-mail
|
||||
SMTP_PORT=2500
|
||||
SMTP_USER=fake_mail_user
|
||||
SMTP_PASS=fake_mail_password
|
||||
SMTP_SENDER_NAME=fake_sender
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
## Phone auth
|
||||
ENABLE_PHONE_SIGNUP=true
|
||||
ENABLE_PHONE_AUTOCONFIRM=true
|
||||
|
||||
## OAuth / Social login providers
|
||||
|
||||
# Uncomment and fill in the providers you want to enable.
|
||||
# You must ALSO uncomment the matching GOTRUE_EXTERNAL_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-oauth
|
||||
# GOOGLE_ENABLED=false
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_SECRET=
|
||||
|
||||
# GITHUB_ENABLED=false
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_SECRET=
|
||||
|
||||
# AZURE_ENABLED=false
|
||||
# AZURE_CLIENT_ID=
|
||||
# AZURE_SECRET=
|
||||
|
||||
# Phone / SMS provider configuration
|
||||
# Uncomment to configure SMS delivery for phone auth and phone MFA.
|
||||
# You must ALSO uncomment the matching GOTRUE_SMS_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-phone-mfa
|
||||
# SMS_PROVIDER=twilio
|
||||
# SMS_OTP_EXP=60
|
||||
# SMS_OTP_LENGTH=6
|
||||
# SMS_MAX_FREQUENCY=60s
|
||||
# SMS_TEMPLATE=Your code is {{ .Code }}
|
||||
|
||||
# SMS_TWILIO_ACCOUNT_SID=
|
||||
# SMS_TWILIO_AUTH_TOKEN=
|
||||
# SMS_TWILIO_MESSAGE_SERVICE_SID=
|
||||
|
||||
# Test OTP: map phone numbers to fixed OTP codes for development
|
||||
# Format: phone1:code1,phone2:code2
|
||||
# SMS_TEST_OTP=
|
||||
|
||||
# Multi-factor authentication (MFA)
|
||||
# Uncomment to change MFA defaults.
|
||||
# You must ALSO uncomment the matching GOTRUE_MFA_* lines in docker-compose.yml
|
||||
|
||||
# App Authenticator (TOTP) - enabled by default
|
||||
# MFA_TOTP_ENROLL_ENABLED=true
|
||||
# MFA_TOTP_VERIFY_ENABLED=true
|
||||
|
||||
# Phone MFA - disabled by default (opt-in)
|
||||
# MFA_PHONE_ENROLL_ENABLED=false
|
||||
# MFA_PHONE_VERIFY_ENABLED=false
|
||||
|
||||
# Maximum MFA factors a user can enroll
|
||||
# MFA_MAX_ENROLLED_FACTORS=10
|
||||
|
||||
## SAML SSO
|
||||
|
||||
# You must ALSO uncomment the matching GOTRUE_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso
|
||||
|
||||
# SAML_ENABLED=true
|
||||
# SAML_PRIVATE_KEY=<your-base64-encoded-private-key>
|
||||
|
||||
# Optional: accept encrypted SAML assertions from IdPs (default: false)
|
||||
# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false
|
||||
|
||||
# Optional: how long relay state tokens remain valid (default: 2m0s)
|
||||
# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s
|
||||
|
||||
# Optional: override the SAML entity ID / ACS base URL
|
||||
# Defaults to API_EXTERNAL_URL if not set
|
||||
# SAML_EXTERNAL_URL=https://supabase.example.com:8000
|
||||
|
||||
# Optional: rate limit on the ACS endpoint (requests per second, default: 15)
|
||||
# SAML_RATE_LIMIT_ASSERTION=15
|
||||
|
||||
|
||||
############
|
||||
# Storage - Configuration for Storage
|
||||
############
|
||||
|
||||
# Check the S3_PROTOCOL_ACCESS_KEY_ID/SECRET above, and
|
||||
# refer to the documentation at:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-s3
|
||||
# to learn how to configure the S3 protocol endpoint
|
||||
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET=stub
|
||||
|
||||
# Used for S3 protocol endpoint configuration
|
||||
REGION=stub
|
||||
|
||||
# Used by MinIO when added via:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
MINIO_ROOT_USER=supa-storage
|
||||
MINIO_ROOT_PASSWORD=2cc00cf3c9aa04b63d22b8e9549deae6
|
||||
|
||||
# Equivalent to project_ref as described here:
|
||||
# https://supabase.com/docs/guides/storage/s3/authentication#session-token
|
||||
STORAGE_TENANT_ID=stub
|
||||
|
||||
|
||||
############
|
||||
# Functions - Configuration for Edge functions
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-functions
|
||||
|
||||
# NOTE: VERIFY_JWT applies to all functions
|
||||
FUNCTIONS_VERIFY_JWT=false
|
||||
|
||||
|
||||
############
|
||||
# API - Configuration for PostgREST
|
||||
############
|
||||
|
||||
# Postgres schemas exposed via the REST API
|
||||
PGRST_DB_SCHEMAS=public,storage,graphql_public
|
||||
|
||||
# Max number of rows returned by a request
|
||||
PGRST_DB_MAX_ROWS=1000
|
||||
|
||||
# Extra schemas added to the search_path of every request
|
||||
PGRST_DB_EXTRA_SEARCH_PATH=public
|
||||
|
||||
|
||||
############
|
||||
# Logs and Analytics
|
||||
############
|
||||
|
||||
## Vector log collection and routing
|
||||
|
||||
# Docker socket location - required for proper Vector operation
|
||||
DOCKER_SOCKET_LOCATION=/var/run/docker.sock
|
||||
# For Podman use the following:
|
||||
# DOCKER_SOCKET_LOCATION=/run/podman/podman.sock
|
||||
|
||||
## Analytics (Logflare)
|
||||
|
||||
# Check the LOGFLARE_* access token configuration _above_.
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
|
||||
# Google Cloud Project details
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/reference/self-hosting-analytics/introduction
|
||||
GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
|
||||
GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
|
||||
|
||||
|
||||
############
|
||||
# API gateway
|
||||
############
|
||||
|
||||
# Kong configuration variables
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# Used internally by the API gateway - DO NOT use in any client or server code.
|
||||
# Pre-signed ES256 JWT "API key" for anon role.
|
||||
ANON_KEY_ASYMMETRIC=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVkMTM0OTZhLTUzZGYtNDNlOC1hZmIzLTIwMTZmZTIxNzUwOCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzgwNjY1NDk0LCJleHAiOjE5MzgzNDU0OTR9.rhh-E42KhEp6F_znI-fT9PrBFOANyk89xuzcdlwrMfQ5tBVnHr-oHt088yLn0G_qzpuG-3p8L3RpCBCjiQC3yw
|
||||
# Pre-signed ES256 JWT "API key" for service_role.
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVkMTM0OTZhLTUzZGYtNDNlOC1hZmIzLTIwMTZmZTIxNzUwOCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3ODA2NjU0OTQsImV4cCI6MTkzODM0NTQ5NH0.qaoG8kfkwXDw1ap0R3k1mqn-NLItjxxrOn9BWiXXwIs40dXTfcW1hEx2NlUWKR_-VJuFg1GpiS3kBrrGs-WqjA
|
||||
|
||||
|
||||
############
|
||||
# imgproxy
|
||||
############
|
||||
|
||||
# Enable webp support
|
||||
IMGPROXY_AUTO_WEBP=true
|
||||
|
||||
|
||||
############
|
||||
# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https
|
||||
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
|
||||
# Domain name for the proxy (must point to your server)
|
||||
PROXY_DOMAIN=your-domain.example.com
|
||||
|
||||
# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN).
|
||||
# This should be a valid email, not a placehoder (otherwise Certbot may fail to start).
|
||||
CERTBOT_EMAIL=admin@example.com
|
||||
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
* text=auto
|
||||
|
||||
*.md eol=lf
|
||||
|
||||
*.env eol=lf
|
||||
.env.example eol=lf
|
||||
|
||||
*.sh eol=lf
|
||||
*.sql eol=lf
|
||||
*.yml eol=lf
|
||||
*.yaml eol=lf
|
||||
*.ts eol=lf
|
||||
*.exs eol=lf
|
||||
*.conf eol=lf
|
||||
*.tpl eol=lf
|
||||
Caddyfile eol=lf
|
||||
Dockerfile* eol=lf
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
volumes/db/data
|
||||
volumes/storage
|
||||
.env
|
||||
test.http
|
||||
docker-compose.override.yml
|
||||
538
CHANGELOG.md
Normal file
538
CHANGELOG.md
Normal file
@@ -0,0 +1,538 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Supabase self-hosted Docker configuration.
|
||||
|
||||
Changes are grouped by service rather than by change type. See [versions.md](./versions.md) for complete image version history and rollback information.
|
||||
|
||||
See per-service updates below for details. Only the most important changes relevant to [self-hosted Supabase](https://supabase.com/docs/guides/self-hosting) are included here. For the full list of changes, refer to the release notes and changelogs of each individual service.
|
||||
|
||||
**Note:** Configuration updates marked with "requires [...] update" are already included in the latest version of the repository. Pull the latest changes or refer to the linked PR for manual updates. After updating `docker-compose.yml`, pull the latest images and recreate containers - use `docker compose pull && docker compose down && docker compose up -d`.
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
⚠️ **Upcoming changes:** Check the main Supabase [changelog](https://github.com/orgs/supabase/discussions/categories/changelog?discussions_q=is%3Aopen+category%3AChangelog+label%3Aself-hosted) for updates:
|
||||
|
||||
- [Upgrading from PG 15 to 17 (breaking change)](https://github.com/orgs/supabase/discussions/46080)
|
||||
- [Switching Studio from `supabase_admin` to `postgres` (breaking change)](https://github.com/orgs/supabase/discussions/46081)
|
||||
|
||||
---
|
||||
|
||||
## [2026-06-03]
|
||||
|
||||
⚠️ **Note:** This update includes **important changes**. Please check the details below.
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Logs and analytics are now [optional](https://github.com/orgs/supabase/discussions/46084) and were removed from the default `docker-compose.yml`. A new `docker-compose.logs.yml` override has been added. Check the main [configuration guide](https://supabase.com/docs/guides/self-hosting/docker#enabling-analytics) and the changes to Studio below for more information - PR [#45327](https://github.com/supabase/supabase/pull/45327) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
- ⚠️ Added `COMPOSE_FILE` to `.env.example` for configuring compose overrides (also used by `run.sh`) - PR [#45603](https://github.com/supabase/supabase/pull/45603)
|
||||
|
||||
### Documentation
|
||||
- Added a new [reference list](https://github.com/supabase/supabase/blob/master/docker/CONFIG.md) of all configuration environment variables - PR [#46124](https://github.com/supabase/supabase/pull/46124)
|
||||
- Updated the main installation and configuration [guide](https://supabase.com/docs/guides/self-hosting/docker) (added "quick start" path and opt-in for logs and analytics; removed the legacy JWT secrets generator) - PR [#46416](https://github.com/supabase/supabase/pull/46416), PR [#45359](https://github.com/supabase/supabase/pull/45359)
|
||||
- Updated the logs and analytics [how-to guide](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#46452](https://github.com/supabase/supabase/pull/46452)
|
||||
|
||||
### Utils
|
||||
- Added `setup.sh` and `run.sh` to support quick start and easier management of the compose configuration - PR [#45603](https://github.com/supabase/supabase/pull/45603)
|
||||
- Updated `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-key.sh` to remove the dependency on OpenSSL and Node.js - PR [#45941](https://github.com/supabase/supabase/pull/45941)
|
||||
- Updated `tests/test-container-logs.sh` to skip checks for `kong`, `analytics` and `vector` when the services are not running - PR [#46099](https://github.com/supabase/supabase/pull/46099)
|
||||
|
||||
### API gateway
|
||||
- Updated Envoy version to `1.38.0` (see `docker-compose.envoy.yml`) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
|
||||
- Updated Envoy configuration to address a discrepancy in API key checking (requires `volumes/api/envoy` update) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.06.03-sha-0bca601`
|
||||
- ⚠️ Added `ENABLED_FEATURES_LOGS_ALL` to Studio service configuration (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
- ⚠️ Added `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` to Studio service configuration (requires `docker-compose.yml` update) - PR [#46173](https://github.com/supabase/supabase/pull/46173)
|
||||
- ⚠️ Added `start_period` to Studio healthcheck for more reliable cold-boot on slower hosts (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
- Fixed incorrect connection strings in the connect sheet for self-hosted environments - PR [#46217](https://github.com/supabase/supabase/pull/46217)
|
||||
- Updated project home and functions page, and added a minimal project settings implementation - PR [#46544](https://github.com/supabase/supabase/pull/46544), PR [#46550](https://github.com/supabase/supabase/pull/46550), PR [#46554](https://github.com/supabase/supabase/pull/46554)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.189.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.189.0)
|
||||
- ⚠️ Added `GOTRUE_JWT_ISSUER` to Auth service configuration (requires `docker-compose.yml` update) - PR [#46020](https://github.com/supabase/supabase/pull/46020)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.12` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.12)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.102.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.102.3)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.60.4` - [Release](https://github.com/supabase/storage/releases/tag/v1.60.4)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.96.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.6)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.74.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.74.0)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.9.5` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.9.5)
|
||||
- Added `POSTGRES_HOST` to Supavisor service configuration (requires `docker-compose.yml` and `volumes/pooler/pooler.exs` update) - PR [#41273](https://github.com/supabase/supabase/pull/41273)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.43.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.43.1)
|
||||
- ⚠️ Changed default `docker-compose.yml` to no longer include logs & analytics. Read more in Supabase's [changelog](https://github.com/orgs/supabase/discussions/46084) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
|
||||
---
|
||||
|
||||
## [2026-04-27]
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added `docker-compose.envoy.yml` and `volumes/api/envoy`. See also the API gateway updates below - PR [#43838](https://github.com/supabase/supabase/pull/43838)
|
||||
- ⚠️ Changed Studio healthcheck and some other configuration for better compatibility with Podman (requires `docker-compose.yml` update) - PR [#44754](https://github.com/supabase/supabase/pull/44754)
|
||||
- ⚠️ Changed Studio configuration to bind to all IPv4 interfaces only (requires `docker-compose.yml` update) - PR [#44772](https://github.com/supabase/supabase/pull/44772)
|
||||
|
||||
### Documentation
|
||||
- Added a new [how-to](https://supabase.com/docs/guides/self-hosting/remove-superuser-access) describing how to switch from `supabase_admin` to `postgres` role for Studio - PR [#42975](https://github.com/supabase/supabase/pull/42975) (via [@singh-inder](https://github.com/singh-inder/))
|
||||
- Added a new [how-to](https://github.com/supabase/supabase/pull/45152) for configuring Envoy as the new API gateway - PR [#45152](https://github.com/supabase/supabase/pull/45152)
|
||||
- Updated the main [setup guide](https://supabase.com/docs/guides/self-hosting/docker) and the how-tos to reflect the state of the self-hosted Supabase configuration - PR [#45011](https://github.com/supabase/supabase/pull/45011)
|
||||
|
||||
### Utils
|
||||
- ⚠️ Added `utils/reassign-owner.sh` to update database objects. Read more in the "[Remove superuser access](https://supabase.com/docs/guides/self-hosting/remove-superuser-access)" how-to guide - PR [#42975](https://github.com/supabase/supabase/pull/42975)
|
||||
- ⚠️ Changed `utils/add-new-auth-keys.sh` to also update `docker-compose.yml` - PR [#45056](https://github.com/supabase/supabase/pull/45056)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Added Envoy as the new optional API gateway (requires `docker-compose.envoy.yml`, `volumes/api/envoy`, and `volumes/logs/vector.yml` update) - PR [#43838](https://github.com/supabase/supabase/pull/43838) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.04.27-sha-5f60601`
|
||||
- ⚠️ Added 4 new lints to the Security Advisor. Read more about lint rules 0026 - 0029 in the [Performance and Security Advisors](https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0026_pg_graphql_anon_table_exposed) section of the Supabase documentation - PR [#45253](https://github.com/supabase/supabase/pull/45253), PR [#45260](https://github.com/supabase/supabase/pull/45260)
|
||||
---
|
||||
|
||||
## [2026-04-08]
|
||||
|
||||
### Documentation
|
||||
- Added new how-to guides for configuring [custom email templates](https://supabase.com/docs/guides/self-hosting/custom-email-templates), setting up [SAML SSO](https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso), and [using Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) - PR [#42832](https://github.com/supabase/supabase/pull/42832), PR [#43386](https://github.com/supabase/supabase/pull/43386), PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
|
||||
### Utils
|
||||
- ⚠️ Added `utils/upgrade-pg17.sh`. Read more in the "[Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17)" how-to guide - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Added configuration for SAML SSO (requires `.env`, `docker-compose.yml` and `volumes/api/kong.yml` update) - PR [#43385](https://github.com/supabase/supabase/pull/43385) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.04.08-sha-205cbe7`
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.8` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.8)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.48.26` - [Release](https://github.com/supabase/storage/releases/tag/v1.48.26)
|
||||
|
||||
### imgproxy
|
||||
- Changed `IMGPROXY_ENABLE_WEBP_DETECTION` environment variable to `IMGPROXY_AUTO_WEBP` (requires `.env` and `docker-compose.yml` update) - PR [#43919](https://github.com/supabase/supabase/pull/43919)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.96.3` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.36.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.36.1)
|
||||
|
||||
### Postgres
|
||||
- ⚠️ Added `docker-compose.pg17.yml` override - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
- ⚠️ Added `utils/upgrade-pg17.sh` - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
- ⚠️ Added [documentation](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) explaining the upgrade to Postgres 17
|
||||
|
||||
---
|
||||
|
||||
## [2026-03-16]
|
||||
|
||||
⚠️ **Note:** This update includes **important changes**. Please check the details below. The following configuration files have been added/updated: `utils/add-new-auth-keys.sh`, `utils/rotate-new-api-keys.sh`, `docker-compose.yml`, `.env.example`, `docker-compose.s3.yml`, `docker-compose.rustfs.yml`, `volumes/api/kong.yml`, `volumes/api/kong-entrypoint.sh`, `docker-compose.caddy.yml`, `docker-compose.nginx.yml`, `volumes/functions/main/index.ts`, and `volumes/proxy`.
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added scripts and templates to support the new API key format (`sb_` API keys) and the new asymmetric authentication. Check the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys) for detailed instructions - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
- Added optional proxy configuration for Caddy and nginx. Read the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https) to learn more - PR [#43291](https://github.com/supabase/supabase/pull/43291)
|
||||
|
||||
### Documentation
|
||||
- Added several new how-to guides to the self-hosted Supabase [documentation](https://supabase.com/docs/guides/self-hosting) - PR [#42745](https://github.com/supabase/supabase/pull/42745), PR [#42953](https://github.com/supabase/supabase/pull/42953), PR [#43177](https://github.com/supabase/supabase/pull/43177), PR [#43286](https://github.com/supabase/supabase/pull/43286), PR [#43293](https://github.com/supabase/supabase/pull/43293)
|
||||
|
||||
### Utils and tests
|
||||
- Added `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-keys.sh` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
- Added `tests/` with 100+ test cases - PR [#43573](https://github.com/supabase/supabase/pull/43573)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.03.16-sha-5528817`
|
||||
- ⚠️ Added the link to the Data API page in Integrations - PR [#43268](https://github.com/supabase/supabase/pull/43268)
|
||||
- ⚠️ Added `PGRST_DB_SCHEMAS`, `PGRST_DB_EXTRA_SEARCH_PATH`, and `PGRST_DB_MAX_ROWS` to Studio configuration (requires `docker-compose.yml` update) - PR [#43268](https://github.com/supabase/supabase/pull/43268)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.7.0` - [Release](https://github.com/supabase-community/supabase-mcp/releases/tag/v0.7.0)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Updated Kong to `3.9.1` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.6` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.6)
|
||||
|
||||
### Realtime
|
||||
- ⚠️ Added **mandatory** `METRICS_JWT_SECRET` environment variable (requires `docker-compose.s3.yml` update) - PR [realtime#1729](https://github.com/supabase/realtime/pull/1729)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.44.2` - [Release](https://github.com/supabase/storage/releases/tag/v1.44.2)
|
||||
- ⚠️ Added `STORAGE_PUBLIC_URL` environment variable to simplify proxy configuration (requires `docker-compose.s3.yml` update) - PR [storage#900](https://github.com/supabase/storage/pull/900)
|
||||
- ⚠️ Added RustFS as an optional S3 backend - PR [#42935](https://github.com/supabase/supabase/pull/42935)
|
||||
- ⚠️ Changed Docker Compose configuration for S3 backends to use named volumes - PR [#43815](https://github.com/supabase/supabase/pull/43815)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.71.2` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.71.2)
|
||||
- ⚠️ Added `SUPABASE_PUBLISHABLE_KEYS`, `SUPABASE_SECRET_KEYS`, and `SUPABASE_PUBLIC_URL` environment variables (requires `docker-compose.yml` update)
|
||||
- ⚠️ Added an option for a "hybrid" JWT verification following the addition of the new API keys and the new asymmetric authentication (requires `volumes/functions/main/index.ts` update) - PR [#42130](https://github.com/supabase/supabase/pull/42130)
|
||||
- ⚠️ Added optional rate limiter - PR [edge-runtime#670](https://github.com/supabase/edge-runtime/pull/670)
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-18]
|
||||
|
||||
### Storage
|
||||
- Changed MinIO image to use Chainguard [minio](https://images.chainguard.dev/directory/image/minio/overview) and [minio-client](https://images.chainguard.dev/directory/image/minio-client/overview) (requires `docker-compose.s3.yml` update) - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
- Updated Storage image version to `v1.37.8` in `docker-compose.s3.yml`
|
||||
- Removed `imgproxy` service from `docker-compose.s3.yml` to minimize redundancy - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
- Fixed inconsistent `storage` service entry ordering in `docker-compose.yml` and `docker-compose.s3.yml` to improve diff readability - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
|
||||
### Edge Runtime
|
||||
- Added a `deno-cache` named volume to avoid re-downloading dependencies (requires `docker-compose.yml` and `volumes/functions/*` update) - PR [#40822](https://github.com/supabase/supabase/pull/40822)
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-16]
|
||||
|
||||
⚠️ **Note:** This update includes several breaking changes, including a security fix for Analytics. Please check the details below. The following configuration files have been updated: `docker-compose.yml`, `.env.example`, `docker-compose.s3.yml`, `volumes/api/kong.yml`, and `volumes/logs/vector.yml`.
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.02.16-sha-26c615c`
|
||||
- Added Edge Functions management UI (requires `docker-compose.yml` update) - PR [#40690](https://github.com/supabase/supabase/pull/40690), PR [#42322](https://github.com/supabase/supabase/pull/42322), PR [#42349](https://github.com/supabase/supabase/pull/42349), PR [#42350](https://github.com/supabase/supabase/pull/42350)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.6.3` - [Release](https://github.com/supabase-community/supabase-mcp/releases/tag/v0.6.3)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.186.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.186.0)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.5` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.5)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.76.5` - [Release](https://github.com/supabase/realtime/releases/tag/v2.76.5)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.37.8` - [Release](https://github.com/supabase/storage/releases/tag/v1.37.8)
|
||||
- ⚠️ Changed environment variable configuration for Storage (requires `docker-compose.yml`, `.env.example` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185), PR [#42862](https://github.com/supabase/supabase/pull/42862)
|
||||
- ⚠️ Added **default** configuration to access buckets via `/storage/v1/s3` endpoint (requires `docker-compose.yml` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185)
|
||||
- ⚠️ Changed MinIO configuration for the S3 backend (requires `docker-compose.s3.yml` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.70.3` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.31.2` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.31.2)
|
||||
- ⚠️ Changed default configuration to disable Logflare on `0.0.0.0:4000` to prevent access to `/dashboard` (requires `docker-compose.yml` update). Read more in the "Production Recommendations" section of Logflare [documentation](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
- ⚠️ Changed Kong routes to not include `/analytics/v1` by default (requires `/volumes/api/kong.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
|
||||
### Vector
|
||||
- Updated to `0.53.0-alpine` - [Changelog](https://vector.dev/releases/0.53.0/) | [Release](https://github.com/vectordotdev/vector/releases/tag/v0.53.0)
|
||||
- ⚠️ Major version jump from `0.28.1` (requires `volumes/logs/vector.yml` update) - PR [#42525](https://github.com/supabase/supabase/pull/42525)
|
||||
- ⚠️ Changed Postgres sink configuration to bypass Kong (requires `volumes/logs/vector.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
- ⚠️ Changed retry settings for all sinks to increase timeouts (requires `volumes/logs/vector.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-05]
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.37.1` - [Release](https://github.com/supabase/storage/releases/tag/v1.37.1)
|
||||
- Fixed an issue with Storage not starting because of an issue with migrations - PR [storage#845](https://github.com/supabase/storage/pull/845)
|
||||
|
||||
---
|
||||
|
||||
## [2026-01-27]
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.01.27-sha-6aa59ff`
|
||||
- Added SQL snippets (requires `docker-compose.yml` update) - PR [#41112](https://github.com/supabase/supabase/pull/41112), PR [#41557](https://github.com/supabase/supabase/pull/41557), discussion [#42031](https://github.com/orgs/supabase/discussions/42031)
|
||||
- Fixed type generator - PR [#40481](https://github.com/supabase/supabase/pull/40481)
|
||||
- Fixed minor UI discrepancies - PR [#40579](https://github.com/supabase/supabase/pull/40579), PR [#41936](https://github.com/supabase/supabase/pull/41936), PR [#41970](https://github.com/supabase/supabase/pull/41970), PR [#41971](https://github.com/supabase/supabase/pull/41971), PR [#41972](https://github.com/supabase/supabase/pull/41972), PR [#42015](https://github.com/supabase/supabase/pull/42015)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.185.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.185.0)
|
||||
- ⚠️ Fixed security-related issues
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.3` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.3)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.72.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.72.0)
|
||||
- Changed healthchecks logging to off by default (requires `docker-compose.yml` update) - PR [realtime#1677](https://github.com/supabase/realtime/pull/1677), PR [#42156](https://github.com/supabase/supabase/pull/42156)
|
||||
- Changed logging configuration and healthcheck frequency to reduce log volume (requires `docker-compose.yml` update) - PR [#42112](https://github.com/supabase/supabase/pull/42112)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.33.5` - [Release](https://github.com/supabase/storage/releases/tag/v1.33.5)
|
||||
|
||||
### imgproxy
|
||||
- Updated to `v3.30.1` - [Changelog](https://github.com/imgproxy/imgproxy/blob/master/CHANGELOG.md) | [Release](https://github.com/imgproxy/imgproxy/releases/tag/v3.30.1)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.95.2` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.95.2)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.70.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.0)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.30.3` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.30.3)
|
||||
|
||||
### Postgres
|
||||
- No image update
|
||||
- Fixed Postgres logging configuration (requires `volumes/logs/vector.yml` update) - PR [#41800](https://github.com/supabase/supabase/pull/41800)
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-18]
|
||||
|
||||
### Documentation
|
||||
- Updated self-hosting installation and configuration guide - PR [#40901](https://github.com/supabase/supabase/pull/40901), PR [#41438](https://github.com/supabase/supabase/pull/41438)
|
||||
|
||||
### Utils
|
||||
- Added `utils/generate-keys.sh` - PR [#41363](https://github.com/supabase/supabase/pull/41363)
|
||||
- Added `utils/db-passwd.sh` - PR [#41432](https://github.com/supabase/supabase/pull/41432)
|
||||
- Changed `reset.sh` to POSIX and added more checks - PR [#41361](https://github.com/supabase/supabase/pull/41361)
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.12.17-sha-43f4f7f`
|
||||
- ⚠️ Fixed additional issues related to [React2Shell](https://vercel.com/kb/bulletin/react2shell)
|
||||
- Fixed an issue with the Users page not being updated on changes - PR [#41254](https://github.com/supabase/supabase/pull/41254)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.5.10` - [Release](https://github.com/supabase-community/supabase-mcp/releases/tag/v0.5.10)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.184.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.184.0)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.95.1` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.95.1)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.27.0` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.27.0)
|
||||
- Fixed multiple issues, including a race condition
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-10]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.12.09-sha-434634f`
|
||||
- ⚠️ Fixed security issues related to [React2Shell](https://vercel.com/kb/bulletin/react2shell)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.5.9` - [Release](https://github.com/supabase-community/supabase-mcp/releases/tag/v0.5.9)
|
||||
- ⚠️ Changed MCP tool `get_anon_key` to `get_publishable_keys`
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.1` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.1)
|
||||
- ⚠️ **Major upgrade from v13.x to v14.x** - please report any unexpected behavior
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.68.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.68.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.33.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.33.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.28` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.28)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.25` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.25)
|
||||
|
||||
---
|
||||
|
||||
## [2025-12-08]
|
||||
|
||||
### Realtime
|
||||
- No image update
|
||||
- Changed boolean values to strings in Docker Compose for better compatibility with Podman - PR [#40994](https://github.com/supabase/supabase/pull/40994), also PR [realtime#1614](https://github.com/supabase/realtime/pull/1614)
|
||||
- Changed healthcheck in Docker Compose for better compatibility with Podman - PR [#41159](https://github.com/supabase/supabase/pull/41159)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-26]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.26-sha-8f096b5`
|
||||
- Fixed MCP `get_advisors` tool - PR [#40783](https://github.com/supabase/supabase/pull/40783)
|
||||
- Fixed AI Assistant request schema - PR [#40830](https://github.com/supabase/supabase/pull/40830)
|
||||
- Fixed log drains page - PR [#40835](https://github.com/supabase/supabase/pull/40835)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.65.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13)
|
||||
- Fixed crashdump when `POSTGRES_BACKEND_URL` is malformed - PR [logflare#2954](https://github.com/Logflare/logflare/pull/2954)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-25]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.24-sha-d990ae8` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40734)
|
||||
- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - PR [#40078](https://github.com/supabase/supabase/pull/40078)
|
||||
- Fixed parameterized SQL queries in MCP tools - PR [#40499](https://github.com/supabase/supabase/pull/40499)
|
||||
- Fixed Studio showing paid options for log drains - PR [#40510](https://github.com/supabase/supabase/pull/40510)
|
||||
- Fixed AI Assistant authentication - PR [#40654](https://github.com/supabase/supabase/pull/40654)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.183.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.183.0)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.65.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.2)
|
||||
- Fixed handling of boolean configuration options - PR [realtime#1614](https://github.com/supabase/realtime/pull/1614)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.32.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.32.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.25` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.25)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
|
||||
- Fixed Auth logs query - PR [logflare#2936](https://github.com/Logflare/logflare/pull/2936)
|
||||
- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - PR [logflare#2942](https://github.com/Logflare/logflare/pull/2942)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-17]
|
||||
|
||||
### Storage
|
||||
- No image update
|
||||
- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - PR [#40500](https://github.com/supabase/supabase/pull/40500)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-12]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.10-sha-5291fe3` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
|
||||
- Added log drains - PR [#28297](https://github.com/supabase/supabase/pull/28297)
|
||||
- Fixed Studio using `postgres` role instead of `supabase_admin` - PR [#39946](https://github.com/supabase/supabase/pull/39946)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.182.1` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md#21821-2025-11-05) | [Release](https://github.com/supabase/auth/releases/tag/v2.182.1)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.63.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.63.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.29.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.29.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.23` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.23)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
|
||||
|
||||
---
|
||||
|
||||
## [2025-11-05]
|
||||
|
||||
### Studio
|
||||
- No image update
|
||||
- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - PR [#40169](https://github.com/supabase/supabase/pull/40169)
|
||||
|
||||
### Realtime
|
||||
- No image update
|
||||
- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - PR [#39963](https://github.com/supabase/supabase/pull/39963)
|
||||
|
||||
---
|
||||
|
||||
## [2025-10-28]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.27-sha-85b84e0` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
|
||||
- Fixed broken authentication when uploading files to Storage - PR [#39829](https://github.com/supabase/supabase/pull/39829)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.57.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.57.2)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.2` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.2)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.93.1` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.1)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.15` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.15)
|
||||
|
||||
---
|
||||
|
||||
## [2025-10-27]
|
||||
|
||||
### Studio
|
||||
- No image update
|
||||
- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - PR [#39849](https://github.com/supabase/supabase/pull/39849)
|
||||
- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - PR [#39952](https://github.com/supabase/supabase/pull/39952)
|
||||
|
||||
---
|
||||
|
||||
## [2025-10-21]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.20-sha-5005fc6` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
|
||||
- Fixed issues with Edge Functions and cron logs not being visible in Studio - PR [#39388](https://github.com/supabase/supabase/pull/39388), PR [#39704](https://github.com/supabase/supabase/pull/39704), PR [#39711](https://github.com/supabase/supabase/pull/39711)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.56.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.56.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.1` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.1)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.93.0` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.14)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
|
||||
|
||||
---
|
||||
|
||||
## [2025-10-13]
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
|
||||
|
||||
---
|
||||
|
||||
## [2025-10-08]
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.01-sha-8460121` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
|
||||
- Added "local" remote MCP server - PR [#38797](https://github.com/supabase/supabase/pull/38797), PR [#39041](https://github.com/supabase/supabase/pull/39041)
|
||||
- ⚠️ Changed Studio connection method to `postgres-meta` - affects non-standard database port configurations
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.180.0` - [Release](https://github.com/supabase/auth/releases/tag/v2.180.0)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v13.0.7` - [Release](https://github.com/PostgREST/postgrest/releases/tag/v13.0.7) | [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.51.11` - [Release](https://github.com/supabase/realtime/releases/tag/v2.51.11)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.0)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.91.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.91.6)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
|
||||
|
||||
### Postgres
|
||||
- Updated to `15.8.1.085` - [Release](https://github.com/supabase/postgres/releases/tag/15.8.1.085)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.0` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.0)
|
||||
|
||||
---
|
||||
95
README.md
Normal file
95
README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://deepwiki.com/supabase/supabase/3-self-hosted-deployment)
|
||||
|
||||
</div>
|
||||
|
||||
# Self-Hosted Supabase with Docker
|
||||
|
||||
This is the official Docker Compose setup for self-hosted Supabase. It provides a complete stack with all Supabase services running locally or on your infrastructure.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow the detailed setup guide in our documentation: [Self-Hosting with Docker](https://supabase.com/docs/guides/self-hosting/docker)
|
||||
|
||||
The guide covers:
|
||||
- Prerequisites (Git and Docker)
|
||||
- Initial setup and configuration
|
||||
- Securing your installation
|
||||
- Accessing services
|
||||
- Updating your instance
|
||||
|
||||
## What's Included
|
||||
|
||||
This Docker Compose configuration includes the following services:
|
||||
|
||||
- **[Studio](https://github.com/supabase/supabase/tree/master/apps/studio)** - A dashboard for managing your self-hosted Supabase project
|
||||
- **[Kong](https://github.com/Kong/kong)** - Kong API gateway
|
||||
- **[Auth](https://github.com/supabase/auth)** - JWT-based authentication API for user sign-ups, logins, and session management
|
||||
- **[PostgREST](https://github.com/PostgREST/postgrest)** - Web server that turns your PostgreSQL database directly into a RESTful API
|
||||
- **[Realtime](https://github.com/supabase/realtime)** - Elixir server that listens to PostgreSQL database changes and broadcasts them over websockets
|
||||
- **[Storage](https://github.com/supabase/storage)** - RESTful API for managing files in S3, with Postgres handling permissions
|
||||
- **[imgproxy](https://github.com/imgproxy/imgproxy)** - Fast and secure image processing server
|
||||
- **[postgres-meta](https://github.com/supabase/postgres-meta)** - RESTful API for managing Postgres (fetch tables, add roles, run queries)
|
||||
- **[PostgreSQL](https://github.com/supabase/postgres)** - Object-relational database with over 30 years of active development
|
||||
- **[Edge Runtime](https://github.com/supabase/edge-runtime)** - Web server based on Deno runtime for running JavaScript, TypeScript, and WASM services
|
||||
- **[Logflare](https://github.com/Logflare/logflare)** - Log management and event analytics platform
|
||||
- **[Vector](https://github.com/vectordotdev/vector)** - High-performance observability data pipeline for logs
|
||||
- **[Supavisor](https://github.com/supabase/supavisor)** - Supabase's Postgres connection pooler
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Self-Hosting with Docker](https://supabase.com/docs/guides/self-hosting/docker)** - Setup and configuration guides
|
||||
- **[CHANGELOG.md](./CHANGELOG.md)** - Track recent updates and changes to services
|
||||
- **[versions.md](./versions.md)** - Complete history of Docker image versions for rollback reference
|
||||
- **[Ask DeepWiki / Supabase](https://deepwiki.com/supabase/supabase/3-self-hosted-deployment)** - DeepWiki-generated description of self-hosted configuration
|
||||
- **[CONFIG.md](./CONFIG.md)** - Configuration reference for all environment variables
|
||||
|
||||
## Updates
|
||||
|
||||
To update your self-hosted Supabase instance:
|
||||
|
||||
1. Review [CHANGELOG.md](./CHANGELOG.md) for breaking changes
|
||||
2. Check [versions.md](./versions.md) for new image versions
|
||||
3. Update `docker-compose.yml` if there are configuration changes
|
||||
4. Pull the latest images: `docker compose pull`
|
||||
5. Stop services: `docker compose down`
|
||||
6. Start services with new configuration: `docker compose up -d`
|
||||
|
||||
**Note:** Consider to always backup your database before updating.
|
||||
|
||||
## Community & Support
|
||||
|
||||
For troubleshooting common issues, see:
|
||||
- [GitHub Discussions](https://github.com/orgs/supabase/discussions?discussions_q=is%3Aopen+label%3Aself-hosted) - Questions, feature requests, and workarounds
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues?q=is%3Aissue%20state%3Aopen%20label%3Aself-hosted) - Known issues
|
||||
- [Documentation](https://supabase.com/docs/guides/self-hosting) - Setup and configuration guides
|
||||
|
||||
Self-hosted Supabase is community-supported. Get help and connect with other users:
|
||||
|
||||
- [Discord](https://discord.supabase.com) - Real-time chat and community support
|
||||
- [Reddit](https://www.reddit.com/r/Supabase/) - Official Supabase subreddit
|
||||
|
||||
Share your self-hosting experience:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/supabase/discussions/39820) - "Self-hosting: What's working (and what's not)?"
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Security
|
||||
|
||||
⚠️ **The default configuration is not secure for production use.**
|
||||
|
||||
Before deploying to production, you must:
|
||||
- [Update](https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase) all default passwords and secrets in the `.env` file
|
||||
- Review and update CORS settings
|
||||
- Consider setting up a secure proxy in front of self-hosted Supabase
|
||||
- Review and adjust network security configuration (ACLs, etc.)
|
||||
- Set up proper backup procedures
|
||||
|
||||
See the [main installation guide](https://supabase.com/docs/guides/self-hosting/docker) and the how-tos in the documentation.
|
||||
|
||||
## License
|
||||
|
||||
This repository is licensed under the Apache 2.0 License. See the main [Supabase repository](https://github.com/supabase/supabase) for details.
|
||||
48
dev/data.sql
Normal file
48
dev/data.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by the owner."
|
||||
on profiles for select
|
||||
using ( auth.uid() = id );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can update an avatar."
|
||||
on storage.objects for update
|
||||
with check ( bucket_id = 'avatars' );
|
||||
44
dev/docker-compose.dev.yml
Normal file
44
dev/docker-compose.dev.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
studio:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: apps/studio/Dockerfile
|
||||
target: dev
|
||||
ports:
|
||||
- 8082:8082
|
||||
develop:
|
||||
watch:
|
||||
- action: sync
|
||||
path: ../apps/studio
|
||||
target: /app/apps/studio
|
||||
ignore:
|
||||
- node_modules/
|
||||
- action: rebuild
|
||||
path: package.json
|
||||
|
||||
mail:
|
||||
container_name: supabase-mail
|
||||
image: inbucket/inbucket:3.0.3
|
||||
ports:
|
||||
- '2500:2500' # SMTP
|
||||
- '9000:9000' # web interface
|
||||
- '1100:1100' # POP3
|
||||
auth:
|
||||
environment:
|
||||
- GOTRUE_SMTP_USER=
|
||||
- GOTRUE_SMTP_PASS=
|
||||
meta:
|
||||
ports:
|
||||
- 5555:8080
|
||||
db:
|
||||
restart: 'no'
|
||||
volumes:
|
||||
# Always use a fresh database when developing
|
||||
- /var/lib/postgresql/data
|
||||
# Seed data should be inserted last (alphabetical order)
|
||||
- ./dev/data.sql:/docker-entrypoint-initdb.d/seed.sql
|
||||
storage:
|
||||
volumes:
|
||||
- /var/lib/storage
|
||||
53
docker-compose.caddy.yml
Normal file
53
docker-compose.caddy.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
services:
|
||||
|
||||
# By default, Kong is used as the API gateway and its ports/env are reset
|
||||
# below so Caddy can terminate TLS in front of it.
|
||||
#
|
||||
# When running Envoy instead, e.g.:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.envoy.yml \
|
||||
# -f docker-compose.caddy.yml up -d
|
||||
# comment out the `kong:` block below and uncomment the `api-gw:` block
|
||||
# (and the matching `depends_on` entry further down) so Caddy sits in front
|
||||
# of Envoy rather than Kong.
|
||||
|
||||
#api-gw:
|
||||
# ports: !reset []
|
||||
|
||||
kong:
|
||||
ports: !reset []
|
||||
environment:
|
||||
KONG_PORT_MAPS: "443:8000,443:8443"
|
||||
|
||||
caddy:
|
||||
container_name: supabase-caddy
|
||||
image: caddy:2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
depends_on:
|
||||
#api-gw:
|
||||
# condition: service_healthy
|
||||
kong:
|
||||
condition: service_healthy
|
||||
studio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PROXY_DOMAIN: ${PROXY_DOMAIN}
|
||||
PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME}
|
||||
PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
PROXY_AUTH_PASSWORD=$$(caddy hash-password --plaintext "$$PROXY_AUTH_PASSWORD") && \
|
||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
volumes:
|
||||
- ./volumes/proxy/caddy:/etc/caddy
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
51
docker-compose.envoy.yml
Normal file
51
docker-compose.envoy.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
# Envoy override for Kong
|
||||
# Usage: docker compose -f docker-compose.yml -f docker-compose.envoy.yml up
|
||||
|
||||
services:
|
||||
# Disable the original Kong service
|
||||
kong:
|
||||
profiles:
|
||||
- disabled
|
||||
|
||||
# Rewire dependencies that require Kong to Envoy
|
||||
functions:
|
||||
depends_on: !override
|
||||
api-gw:
|
||||
condition: service_healthy
|
||||
|
||||
# Envoy API gateway
|
||||
api-gw:
|
||||
container_name: supabase-envoy
|
||||
image: envoyproxy/envoy:v1.38.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
volumes:
|
||||
- ./volumes/api/envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro
|
||||
- ./volumes/api/envoy/cds.yaml:/etc/envoy/cds.yaml:ro
|
||||
- ./volumes/api/envoy/lds.template.yaml:/etc/envoy/lds.template.yaml:ro
|
||||
- ./volumes/api/envoy/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
depends_on:
|
||||
studio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_ROLE_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}
|
||||
entrypoint: ["/bin/sh", "/docker-entrypoint.sh"]
|
||||
healthcheck:
|
||||
# Using a TCP port check because this image does not include curl or wget.
|
||||
test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/127.0.0.1/8000'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- kong
|
||||
- envoy
|
||||
99
docker-compose.logs.yml
Normal file
99
docker-compose.logs.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
# This override adds the following to the self-hosted Supabase configuration:
|
||||
# - Logflare: Log management and event analytics platform
|
||||
# - Vector: High-performance observability data pipeline for logs
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.logs.yml up -d
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
|
||||
LOGFLARE_URL: http://analytics:4000
|
||||
ENABLED_FEATURES_LOGS_ALL: "true"
|
||||
|
||||
analytics:
|
||||
container_name: supabase-analytics
|
||||
image: supabase/logflare:1.43.1
|
||||
restart: unless-stopped
|
||||
#ports:
|
||||
# - 4000:4000
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL -o /dev/null http://localhost:4000/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_NODE_HOST: 127.0.0.1
|
||||
|
||||
DB_USERNAME: supabase_admin
|
||||
DB_DATABASE: _supabase
|
||||
DB_HOSTNAME: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_SCHEMA: _analytics
|
||||
|
||||
# Enable single-tenant mode for Logflare
|
||||
LOGFLARE_SINGLE_TENANT: "true"
|
||||
# Seed Supabase-related metadata
|
||||
LOGFLARE_SUPABASE_MODE: "true"
|
||||
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
|
||||
|
||||
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
|
||||
|
||||
# Comment out the following two variables when switching to
|
||||
# the BigQuery backend for logs
|
||||
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
|
||||
POSTGRES_BACKEND_SCHEMA: _analytics
|
||||
|
||||
# Uncomment to use the BigQuery backend for logs
|
||||
#GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
|
||||
#GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}
|
||||
# Uncomment to use the BigQuery backend for logs (requires gcloud.json
|
||||
# service account key from Google Cloud Console)
|
||||
#volumes:
|
||||
# - ./gcloud.json:/opt/app/rel/logflare/bin/gcloud.json:ro,z
|
||||
|
||||
vector:
|
||||
container_name: supabase-vector
|
||||
image: timberio/vector:0.53.0-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
|
||||
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget --no-verbose --tries=1 --spider http://vector:9001/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
|
||||
command:
|
||||
[
|
||||
"--config",
|
||||
"/etc/vector/vector.yml"
|
||||
]
|
||||
security_opt:
|
||||
- "label=disable"
|
||||
52
docker-compose.nginx.yml
Normal file
52
docker-compose.nginx.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
|
||||
# By default, Kong is used as the API gateway and its ports/env are reset
|
||||
# below so nginx can terminate TLS in front of it.
|
||||
#
|
||||
# When running Envoy instead, e.g.:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.envoy.yml \
|
||||
# -f docker-compose.nginx.yml up -d
|
||||
# comment out the `kong:` block below and uncomment the `api-gw:` block
|
||||
# (and the matching `depends_on` entry further down) so nginx sits in front
|
||||
# of Envoy rather than Kong.
|
||||
|
||||
#api-gw:
|
||||
# ports: !reset []
|
||||
|
||||
kong:
|
||||
ports: !reset []
|
||||
environment:
|
||||
KONG_PORT_MAPS: "443:8000,443:8443"
|
||||
|
||||
nginx:
|
||||
container_name: supabase-nginx
|
||||
image: jonasal/nginx-certbot:6.2.0-nginx1.31.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
#api-gw:
|
||||
# condition: service_healthy
|
||||
kong:
|
||||
condition: service_healthy
|
||||
studio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PROXY_DOMAIN: ${PROXY_DOMAIN}
|
||||
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
|
||||
PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME}
|
||||
PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
printf '%s:%s\n' "$${PROXY_AUTH_USERNAME}" "$$(openssl passwd -apr1 "$${PROXY_AUTH_PASSWORD}")" > /etc/nginx/user_conf.d/dashboard-passwd && \
|
||||
envsubst '$${PROXY_DOMAIN}' < /etc/nginx/supabase-nginx.conf.tpl > /etc/nginx/user_conf.d/nginx.conf && \
|
||||
/scripts/start_nginx_certbot.sh
|
||||
volumes:
|
||||
- ./volumes/proxy/nginx/supabase-nginx.conf.tpl:/etc/nginx/supabase-nginx.conf.tpl:ro
|
||||
- nginx_letsencrypt:/etc/letsencrypt
|
||||
|
||||
volumes:
|
||||
nginx_letsencrypt:
|
||||
15
docker-compose.pg17.yml
Normal file
15
docker-compose.pg17.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Postgres 17 override for self-hosted Supabase.
|
||||
#
|
||||
# Usage (new install or after upgrade):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d
|
||||
#
|
||||
# For upgrading an existing Postgres 15 database, run utils/upgrade-pg17.sh first.
|
||||
#
|
||||
# Note: PG 15 and PG 17 use different postgres UIDs. If starting fresh with a
|
||||
# leftover db-config volume from PG 15, you may see
|
||||
# "FATAL: invalid secret key". Either remove the old volume or fix ownership.
|
||||
# See: docs/guides/self-hosting/postgres-upgrade-17
|
||||
|
||||
services:
|
||||
db:
|
||||
image: supabase/postgres:17.6.1.084
|
||||
47
docker-compose.rustfs.yml
Normal file
47
docker-compose.rustfs.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
|
||||
rustfs:
|
||||
image: rustfs/rustfs
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: ${MINIO_ROOT_USER}
|
||||
RUSTFS_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
|
||||
RUSTFS_CORS_ALLOWED_ORIGINS: "*"
|
||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://rustfs:9000/health" ]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
rustfs-createbucket:
|
||||
image: rustfs/rc
|
||||
depends_on:
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -ec "
|
||||
rc alias set supa-rustfs http://rustfs:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
|
||||
rc mb --ignore-existing supa-rustfs/${GLOBAL_S3_BUCKET}
|
||||
"
|
||||
|
||||
storage:
|
||||
depends_on:
|
||||
rustfs-createbucket:
|
||||
condition: service_completed_successfully
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
STORAGE_BACKEND: s3
|
||||
GLOBAL_S3_ENDPOINT: http://rustfs:9000
|
||||
GLOBAL_S3_PROTOCOL: http
|
||||
GLOBAL_S3_FORCE_PATH_STYLE: true
|
||||
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
||||
volumes:
|
||||
rustfs-data:
|
||||
46
docker-compose.s3.yml
Normal file
46
docker-compose.s3.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
#ports:
|
||||
# - '9000:9000'
|
||||
# - '9001:9001'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
command: server --console-address ":9001" /data
|
||||
healthcheck:
|
||||
test: [ "CMD", "mc", "ready", "local" ]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
|
||||
minio-createbucket:
|
||||
image: cgr.dev/chainguard/minio-client:latest-dev
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -ec "
|
||||
mc alias set supa-minio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
|
||||
mc mb --ignore-existing supa-minio/${GLOBAL_S3_BUCKET}
|
||||
"
|
||||
|
||||
storage:
|
||||
depends_on:
|
||||
minio-createbucket:
|
||||
condition: service_completed_successfully
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
STORAGE_BACKEND: s3
|
||||
GLOBAL_S3_ENDPOINT: http://minio:9000
|
||||
GLOBAL_S3_PROTOCOL: http
|
||||
GLOBAL_S3_FORCE_PATH_STYLE: true
|
||||
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
574
docker-compose.yml
Normal file
574
docker-compose.yml
Normal file
@@ -0,0 +1,574 @@
|
||||
# Usage
|
||||
# Start: docker compose up -d
|
||||
# Stop: docker compose down
|
||||
# Dev mode: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up -d
|
||||
# Reset everything: sh reset.sh
|
||||
#
|
||||
# TODO:
|
||||
# - Podman does not support nested variable interpolation (${A:-${B}})
|
||||
#
|
||||
|
||||
name: supabase
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
container_name: supabase-studio
|
||||
image: supabase/studio:2026.06.03-sha-0bca601
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"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
|
||||
start_period: 20s
|
||||
environment:
|
||||
# Listen on all IPv4 interfaces
|
||||
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}
|
||||
|
||||
# See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access
|
||||
#POSTGRES_USER_READ_WRITE: postgres
|
||||
|
||||
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: 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}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY}
|
||||
|
||||
# See: docker-compose.logs.yml
|
||||
ENABLED_FEATURES_LOGS_ALL: "false"
|
||||
|
||||
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
|
||||
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
|
||||
volumes:
|
||||
- ./volumes/snippets:/app/snippets:z
|
||||
- ./volumes/functions:/app/edge-functions:ro,z
|
||||
|
||||
kong:
|
||||
container_name: supabase-kong
|
||||
image: kong/kong:3.9.1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- api-gw
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
depends_on:
|
||||
studio:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||
volumes:
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||
#- ./volumes/api/server.crt:/home/kong/server.crt:ro
|
||||
#- ./volumes/api/server.key:/home/kong/server.key:ro
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||
# https://github.com/supabase/cli/issues/14
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
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
|
||||
#KONG_SSL_CERT: /home/kong/server.crt
|
||||
#KONG_SSL_CERT_KEY: /home/kong/server.key
|
||||
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}
|
||||
entrypoint: /home/kong/kong-entrypoint.sh
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.189.0
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric)
|
||||
# For Podman, use: GOTRUE_JWT_KEYS: ${JWT_KEYS}
|
||||
GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
|
||||
|
||||
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}/auth/v1
|
||||
|
||||
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}
|
||||
GOTRUE_SMTP_USER: ${SMTP_USER}
|
||||
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
||||
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
|
||||
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
|
||||
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
|
||||
|
||||
# Uncomment to enable OAuth / social login providers.
|
||||
|
||||
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
|
||||
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
|
||||
# GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
|
||||
# GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
|
||||
# GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
||||
# GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# Uncomment to configure SMS delivery (phone auth and phone MFA).
|
||||
# GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
|
||||
# GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
|
||||
# GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
|
||||
# GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
|
||||
# GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
|
||||
|
||||
# Twilio credentials (when SMS_PROVIDER=twilio)
|
||||
# GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
|
||||
# GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
|
||||
# GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
|
||||
|
||||
# Test OTP mappings for development
|
||||
# GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}
|
||||
|
||||
# Uncomment to configure multi-factor authentication (MFA).
|
||||
# GOTRUE_MFA_TOTP_ENROLL_ENABLED: ${MFA_TOTP_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_TOTP_VERIFY_ENABLED: ${MFA_TOTP_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_ENROLL_ENABLED: ${MFA_PHONE_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_VERIFY_ENABLED: ${MFA_PHONE_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_MAX_ENROLLED_FACTORS: ${MFA_MAX_ENROLLED_FACTORS}
|
||||
|
||||
# SAML SSO
|
||||
# GOTRUE_SAML_ENABLED: ${SAML_ENABLED}
|
||||
# GOTRUE_SAML_PRIVATE_KEY: ${SAML_PRIVATE_KEY}
|
||||
# GOTRUE_SAML_ALLOW_ENCRYPTED_ASSERTIONS: ${SAML_ALLOW_ENCRYPTED_ASSERTIONS}
|
||||
# GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD: ${SAML_RELAY_STATE_VALIDITY_PERIOD}
|
||||
# GOTRUE_SAML_EXTERNAL_URL: ${SAML_EXTERNAL_URL}
|
||||
# GOTRUE_SAML_RATE_LIMIT_ASSERTION: ${SAML_RATE_LIMIT_ASSERTION}
|
||||
|
||||
# Uncomment to enable custom access token hook.
|
||||
# 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: "<standard-base64-secret>"
|
||||
|
||||
# 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: supabase-rest
|
||||
image: postgrest/postgrest:v14.12
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
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
|
||||
|
||||
# PostgREST accepts a plain-text symmetric secret, a single JWK, or a JWKS.
|
||||
# For Podman, use either PGRST_JWT_SECRET: ${JWT_SECRET} or
|
||||
# PGRST_JWT_SECRET: ${JWT_JWKS}
|
||||
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
|
||||
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgrest"
|
||||
]
|
||||
|
||||
realtime:
|
||||
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
|
||||
container_name: realtime-dev.supabase-realtime
|
||||
image: supabase/realtime:v2.102.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL --head -o /dev/null -H \"Authorization: Bearer ${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 30s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
PORT: 4000
|
||||
DB_HOST: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: API_JWT_JWKS: ${JWT_JWKS}
|
||||
API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
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"
|
||||
DISABLE_HEALTHCHECK_LOGGING: "true"
|
||||
|
||||
# To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
|
||||
storage:
|
||||
container_name: supabase-storage
|
||||
image: supabase/storage-api:v1.60.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
imgproxy:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://storage:5000/status"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: JWT_JWKS: ${JWT_JWKS}
|
||||
JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
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
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET}
|
||||
# S3 Backend configuration
|
||||
#GLOBAL_S3_ENDPOINT: https://your-s3-endpoint
|
||||
#GLOBAL_S3_PROTOCOL: https
|
||||
#GLOBAL_S3_FORCE_PATH_STYLE: "true"
|
||||
#AWS_ACCESS_KEY_ID: your-access-key-id
|
||||
#AWS_SECRET_ACCESS_KEY: your-secret-access-key
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
TENANT_ID: ${STORAGE_TENANT_ID}
|
||||
# TODO: https://github.com/supabase/storage-api/issues/55
|
||||
REGION: ${REGION}
|
||||
ENABLE_IMAGE_TRANSFORMATION: "true"
|
||||
IMGPROXY_URL: http://imgproxy:5001
|
||||
# S3 protocol endpoint configuration
|
||||
S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID}
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET}
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
|
||||
imgproxy:
|
||||
container_name: supabase-imgproxy
|
||||
image: darthsim/imgproxy:v3.30.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"imgproxy",
|
||||
"health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
IMGPROXY_BIND: ":5001"
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP}
|
||||
IMGPROXY_MAX_SRC_RESOLUTION: 16.8
|
||||
|
||||
meta:
|
||||
container_name: supabase-meta
|
||||
image: supabase/postgres-meta:v0.96.6
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||
PG_META_DB_PORT: ${POSTGRES_PORT}
|
||||
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: supabase-edge-functions
|
||||
image: supabase/edge-runtime:v1.74.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/functions:/home/deno/functions:z
|
||||
- deno-cache:/root/.cache/deno
|
||||
depends_on:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
# New opaque API keys
|
||||
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.
|
||||
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
|
||||
command:
|
||||
[
|
||||
"start",
|
||||
"--main-service",
|
||||
"/home/deno/functions/main"
|
||||
]
|
||||
|
||||
# Comment out everything below this point if you are using an external Postgres database
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:15.8.1.085
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
|
||||
# Must be superuser to create event trigger
|
||||
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
|
||||
# Must be superuser to alter reserved role
|
||||
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
|
||||
# Initialize the database settings with JWT_SECRET and JWT_EXP
|
||||
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
|
||||
# PGDATA directory is persisted between restarts
|
||||
- ./volumes/db/data:/var/lib/postgresql/data:Z
|
||||
# Changes required for internal supabase data such as _analytics
|
||||
- ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
|
||||
# Changes required for Analytics support
|
||||
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
|
||||
# Changes required for Pooler support
|
||||
- ./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"
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
PGPORT: ${POSTGRES_PORT}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PGDATABASE: ${POSTGRES_DB}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgres",
|
||||
"-c",
|
||||
"config_file=/etc/postgresql/postgresql.conf",
|
||||
"-c",
|
||||
"log_min_messages=fatal" # prevents Realtime polling queries from appearing in logs
|
||||
]
|
||||
|
||||
# Update the DATABASE_URL if you are using an external Postgres database
|
||||
supavisor:
|
||||
container_name: supabase-pooler
|
||||
image: supabase/supavisor:2.9.5
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
- ${POOLER_PROXY_PORT_TRANSACTION}:6543
|
||||
volumes:
|
||||
- ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"curl",
|
||||
"-sSfL",
|
||||
"--head",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"http://127.0.0.1:4000/api/health"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
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}@${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}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
REGION: local
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
POOLER_TENANT_ID: ${POOLER_TENANT_ID}
|
||||
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",
|
||||
"-c",
|
||||
"/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
|
||||
]
|
||||
|
||||
volumes:
|
||||
db-config:
|
||||
deno-cache:
|
||||
572
docker-compose.yml.old
Normal file
572
docker-compose.yml.old
Normal file
@@ -0,0 +1,572 @@
|
||||
# Usage
|
||||
# Start: docker compose up -d
|
||||
# Stop: docker compose down
|
||||
# Dev mode: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up -d
|
||||
# Reset everything: sh reset.sh
|
||||
#
|
||||
# TODO:
|
||||
# - Podman does not support nested variable interpolation (${A:-${B}})
|
||||
#
|
||||
|
||||
name: supabase
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
container_name: supabase-studio
|
||||
image: supabase/studio:2026.06.03-sha-0bca601
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"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
|
||||
start_period: 20s
|
||||
environment:
|
||||
# Listen on all IPv4 interfaces
|
||||
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}
|
||||
|
||||
# See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access
|
||||
#POSTGRES_USER_READ_WRITE: postgres
|
||||
|
||||
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: 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}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY}
|
||||
|
||||
# See: docker-compose.logs.yml
|
||||
ENABLED_FEATURES_LOGS_ALL: "false"
|
||||
|
||||
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
|
||||
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
|
||||
volumes:
|
||||
- ./volumes/snippets:/app/snippets:z
|
||||
- ./volumes/functions:/app/edge-functions:ro,z
|
||||
|
||||
kong:
|
||||
container_name: supabase-kong
|
||||
image: kong/kong:3.9.1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- api-gw
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
studio:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||
volumes:
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||
#- ./volumes/api/server.crt:/home/kong/server.crt:ro
|
||||
#- ./volumes/api/server.key:/home/kong/server.key:ro
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||
# https://github.com/supabase/cli/issues/14
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
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
|
||||
#KONG_SSL_CERT: /home/kong/server.crt
|
||||
#KONG_SSL_CERT_KEY: /home/kong/server.key
|
||||
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}
|
||||
entrypoint: /home/kong/kong-entrypoint.sh
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.189.0
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric)
|
||||
# For Podman, use: GOTRUE_JWT_KEYS: ${JWT_KEYS}
|
||||
GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
|
||||
|
||||
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}/auth/v1
|
||||
|
||||
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}
|
||||
GOTRUE_SMTP_USER: ${SMTP_USER}
|
||||
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
||||
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
|
||||
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
|
||||
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
|
||||
|
||||
# Uncomment to enable OAuth / social login providers.
|
||||
# GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
|
||||
# GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
|
||||
# GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
|
||||
# GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
||||
# GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/auth/v1/callback
|
||||
|
||||
# Uncomment to configure SMS delivery (phone auth and phone MFA).
|
||||
# GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
|
||||
# GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
|
||||
# GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
|
||||
# GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
|
||||
# GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
|
||||
|
||||
# Twilio credentials (when SMS_PROVIDER=twilio)
|
||||
# GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
|
||||
# GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
|
||||
# GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
|
||||
|
||||
# Test OTP mappings for development
|
||||
# GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}
|
||||
|
||||
# Uncomment to configure multi-factor authentication (MFA).
|
||||
# GOTRUE_MFA_TOTP_ENROLL_ENABLED: ${MFA_TOTP_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_TOTP_VERIFY_ENABLED: ${MFA_TOTP_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_ENROLL_ENABLED: ${MFA_PHONE_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_VERIFY_ENABLED: ${MFA_PHONE_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_MAX_ENROLLED_FACTORS: ${MFA_MAX_ENROLLED_FACTORS}
|
||||
|
||||
# SAML SSO
|
||||
# GOTRUE_SAML_ENABLED: ${SAML_ENABLED}
|
||||
# GOTRUE_SAML_PRIVATE_KEY: ${SAML_PRIVATE_KEY}
|
||||
# GOTRUE_SAML_ALLOW_ENCRYPTED_ASSERTIONS: ${SAML_ALLOW_ENCRYPTED_ASSERTIONS}
|
||||
# GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD: ${SAML_RELAY_STATE_VALIDITY_PERIOD}
|
||||
# GOTRUE_SAML_EXTERNAL_URL: ${SAML_EXTERNAL_URL}
|
||||
# GOTRUE_SAML_RATE_LIMIT_ASSERTION: ${SAML_RATE_LIMIT_ASSERTION}
|
||||
|
||||
# Uncomment to enable custom access token hook.
|
||||
# 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: "<standard-base64-secret>"
|
||||
|
||||
# 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: supabase-rest
|
||||
image: postgrest/postgrest:v14.12
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
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
|
||||
|
||||
# PostgREST accepts a plain-text symmetric secret, a single JWK, or a JWKS.
|
||||
# For Podman, use either PGRST_JWT_SECRET: ${JWT_SECRET} or
|
||||
# PGRST_JWT_SECRET: ${JWT_JWKS}
|
||||
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
|
||||
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgrest"
|
||||
]
|
||||
|
||||
realtime:
|
||||
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
|
||||
container_name: realtime-dev.supabase-realtime
|
||||
image: supabase/realtime:v2.102.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL --head -o /dev/null -H \"Authorization: Bearer ${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 30s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
PORT: 4000
|
||||
DB_HOST: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: API_JWT_JWKS: ${JWT_JWKS}
|
||||
API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
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"
|
||||
DISABLE_HEALTHCHECK_LOGGING: "true"
|
||||
|
||||
# To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
|
||||
storage:
|
||||
container_name: supabase-storage
|
||||
image: supabase/storage-api:v1.60.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
imgproxy:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://storage:5000/status"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: JWT_JWKS: ${JWT_JWKS}
|
||||
JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
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
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET}
|
||||
# S3 Backend configuration
|
||||
#GLOBAL_S3_ENDPOINT: https://your-s3-endpoint
|
||||
#GLOBAL_S3_PROTOCOL: https
|
||||
#GLOBAL_S3_FORCE_PATH_STYLE: "true"
|
||||
#AWS_ACCESS_KEY_ID: your-access-key-id
|
||||
#AWS_SECRET_ACCESS_KEY: your-secret-access-key
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
TENANT_ID: ${STORAGE_TENANT_ID}
|
||||
# TODO: https://github.com/supabase/storage-api/issues/55
|
||||
REGION: ${REGION}
|
||||
ENABLE_IMAGE_TRANSFORMATION: "true"
|
||||
IMGPROXY_URL: http://imgproxy:5001
|
||||
# S3 protocol endpoint configuration
|
||||
S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID}
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET}
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
|
||||
imgproxy:
|
||||
container_name: supabase-imgproxy
|
||||
image: darthsim/imgproxy:v3.30.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"imgproxy",
|
||||
"health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
IMGPROXY_BIND: ":5001"
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP}
|
||||
IMGPROXY_MAX_SRC_RESOLUTION: 16.8
|
||||
|
||||
meta:
|
||||
container_name: supabase-meta
|
||||
image: supabase/postgres-meta:v0.96.6
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||
PG_META_DB_PORT: ${POSTGRES_PORT}
|
||||
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: supabase-edge-functions
|
||||
image: supabase/edge-runtime:v1.74.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/functions:/home/deno/functions:z
|
||||
- deno-cache:/root/.cache/deno
|
||||
depends_on:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
# New opaque API keys
|
||||
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.
|
||||
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
|
||||
command:
|
||||
[
|
||||
"start",
|
||||
"--main-service",
|
||||
"/home/deno/functions/main"
|
||||
]
|
||||
|
||||
# Comment out everything below this point if you are using an external Postgres database
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:15.8.1.085
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
|
||||
# Must be superuser to create event trigger
|
||||
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
|
||||
# Must be superuser to alter reserved role
|
||||
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
|
||||
# Initialize the database settings with JWT_SECRET and JWT_EXP
|
||||
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
|
||||
# PGDATA directory is persisted between restarts
|
||||
- ./volumes/db/data:/var/lib/postgresql/data:Z
|
||||
# Changes required for internal supabase data such as _analytics
|
||||
- ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
|
||||
# Changes required for Analytics support
|
||||
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
|
||||
# Changes required for Pooler support
|
||||
- ./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"
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
PGPORT: ${POSTGRES_PORT}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PGDATABASE: ${POSTGRES_DB}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgres",
|
||||
"-c",
|
||||
"config_file=/etc/postgresql/postgresql.conf",
|
||||
"-c",
|
||||
"log_min_messages=fatal" # prevents Realtime polling queries from appearing in logs
|
||||
]
|
||||
|
||||
# Update the DATABASE_URL if you are using an external Postgres database
|
||||
supavisor:
|
||||
container_name: supabase-pooler
|
||||
image: supabase/supavisor:2.9.5
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
- ${POOLER_PROXY_PORT_TRANSACTION}:6543
|
||||
volumes:
|
||||
- ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"curl",
|
||||
"-sSfL",
|
||||
"--head",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"http://127.0.0.1:4000/api/health"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
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}@${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}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
REGION: local
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
POOLER_TENANT_ID: ${POOLER_TENANT_ID}
|
||||
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",
|
||||
"-c",
|
||||
"/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
|
||||
]
|
||||
|
||||
volumes:
|
||||
db-config:
|
||||
deno-cache:
|
||||
76
reset.sh
Executable file
76
reset.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
auto_confirm=0
|
||||
|
||||
confirm () {
|
||||
if [ "$auto_confirm" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "Are you sure you want to proceed? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Script canceled."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$1" = "-y" ]; then
|
||||
auto_confirm=1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "*** WARNING: This will remove all containers and container data, and optionally reset .env ***"
|
||||
echo ""
|
||||
|
||||
confirm
|
||||
|
||||
echo "===> Stopping and removing all containers..."
|
||||
|
||||
if [ -f ".env" ]; then
|
||||
docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
elif [ -f ".env.example" ]; then
|
||||
echo "No .env found, using .env.example for docker compose down..."
|
||||
docker compose --env-file .env.example -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
else
|
||||
echo "Skipping 'docker compose down' because there's no env-file."
|
||||
fi
|
||||
|
||||
echo "===> Cleaning up bind-mounted directories..."
|
||||
BIND_MOUNTS="./volumes/db/data ./volumes/storage"
|
||||
|
||||
for dir in $BIND_MOUNTS; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Removing $dir..."
|
||||
confirm
|
||||
rm -rf "$dir"
|
||||
else
|
||||
echo "$dir not found."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "===> Resetting .env file (will save backup to .env.old)..."
|
||||
confirm
|
||||
if [ -f ".env" ] || [ -L ".env" ]; then
|
||||
echo "Renaming existing .env file to .env.old"
|
||||
mv .env .env.old
|
||||
else
|
||||
echo "No .env file found."
|
||||
fi
|
||||
|
||||
if [ -f ".env.example" ]; then
|
||||
echo "===> Copying .env.example to .env"
|
||||
cp .env.example .env
|
||||
else
|
||||
echo "No .env.example found, can't restore .env to default values."
|
||||
fi
|
||||
|
||||
echo "Cleanup complete!"
|
||||
echo "Re-run 'docker compose pull' to update images."
|
||||
echo ""
|
||||
297
run.sh
Executable file
297
run.sh
Executable file
@@ -0,0 +1,297 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Manage the self-hosted Supabase docker compose stack.
|
||||
#
|
||||
# Override files are layered via docker compose's native COMPOSE_FILE env
|
||||
# var in .env. Format: colon-separated list with docker-compose.yml first.
|
||||
#
|
||||
# Examples in .env:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
# Manage with: sh run.sh config add <name> | config remove <name>
|
||||
# (accepts either a short name like 'pg17' or 'docker-compose.pg17.yml')
|
||||
#
|
||||
# Usage:
|
||||
# sh run.sh start # docker compose up -d --wait
|
||||
# sh run.sh stop # docker compose down
|
||||
# sh run.sh restart [service] # restart the stack (or named services)
|
||||
# sh run.sh restart --except <svc>... # restart all services except the named ones
|
||||
# sh run.sh recreate [service] # stop then start (or force-recreate one service)
|
||||
# sh run.sh recreate --except <svc>... # force-recreate all services except the named ones
|
||||
# sh run.sh status # docker compose ps
|
||||
# sh run.sh logs [service] # follow logs (all or one service)
|
||||
# sh run.sh inspect <service> # docker inspect on a service's container
|
||||
# sh run.sh printenv <service> # print a service's environment variables
|
||||
# sh run.sh pull # pull images
|
||||
# sh run.sh config # show the active COMPOSE_FILE list
|
||||
# sh run.sh config add <name> # add an override to COMPOSE_FILE in .env
|
||||
# sh run.sh config remove <name> # remove an override from COMPOSE_FILE in .env
|
||||
# sh run.sh compose-config # dump fully-resolved docker compose config
|
||||
# sh run.sh secrets # print key passwords and API keys from .env
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "ERROR: docker-compose.yml not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalize an override argument:
|
||||
# pg17 -> docker-compose.pg17.yml
|
||||
# docker-compose.pg17.yml -> docker-compose.pg17.yml
|
||||
# ./docker-compose.pg17.yml -> docker-compose.pg17.yml
|
||||
# docker-compose.yml -> error (base file, always implicit)
|
||||
normalize_override() {
|
||||
arg="${1#./}"
|
||||
case "$arg" in
|
||||
docker-compose.yml)
|
||||
echo "ERROR: docker-compose.yml is the base file, always included" >&2
|
||||
return 1
|
||||
;;
|
||||
docker-compose.*.yml)
|
||||
echo "$arg"
|
||||
;;
|
||||
*)
|
||||
echo "docker-compose.${arg}.yml"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Read COMPOSE_FILE from .env (stripping quotes and CR).
|
||||
read_compose_file() {
|
||||
[ -f .env ] || return 0
|
||||
grep '^COMPOSE_FILE=' .env | head -n1 | cut -d= -f2- | tr -d "\r\"'"
|
||||
}
|
||||
|
||||
# Pretty-print the effective compose file list.
|
||||
print_config() {
|
||||
val="$1"
|
||||
[ -z "$val" ] && val="docker-compose.yml"
|
||||
echo "COMPOSE_FILE=$val"
|
||||
echo "compose files:"
|
||||
OLD_IFS=$IFS
|
||||
IFS=:
|
||||
for f in $val; do
|
||||
echo " $f"
|
||||
done
|
||||
IFS=$OLD_IFS
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Update or append COMPOSE_FILE in .env.
|
||||
write_compose_file() {
|
||||
new_value="$1"
|
||||
if [ ! -f .env ]; then
|
||||
echo "ERROR: .env not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
new_line="COMPOSE_FILE=$new_value"
|
||||
if grep -q '^COMPOSE_FILE=' .env; then
|
||||
sed -i.old -e "s|^COMPOSE_FILE=.*$|$new_line|" .env
|
||||
rm -f .env.old
|
||||
else
|
||||
cat >> .env <<EOF
|
||||
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Colon-separated list. Manage with: sh run.sh config add|remove <name>.
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
############
|
||||
$new_line
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Echoes the list of services (one per line) minus those passed as args.
|
||||
# Warns on unknown names; returns 1 if no services remain.
|
||||
services_except() {
|
||||
all_services=$(docker compose config --services)
|
||||
filtered="$all_services"
|
||||
for ex in "$@"; do
|
||||
echo "$all_services" | grep -qFx "$ex" \
|
||||
|| echo "Warning: '$ex' is not a service in this project" >&2
|
||||
filtered=$(echo "$filtered" | grep -vFx "$ex" || true)
|
||||
done
|
||||
if [ -z "$filtered" ]; then
|
||||
echo "No services left after applying --except" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$filtered"
|
||||
}
|
||||
|
||||
CMD="${1:-help}"
|
||||
[ "$#" -gt 0 ] && shift
|
||||
|
||||
case "$CMD" in
|
||||
start|up)
|
||||
exec docker compose up -d --wait "$@"
|
||||
;;
|
||||
stop|down)
|
||||
exec docker compose down "$@"
|
||||
;;
|
||||
restart)
|
||||
if [ "${1:-}" = "--except" ]; then
|
||||
shift
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") restart --except <svc>..." >&2; exit 1; }
|
||||
services=$(services_except "$@") || exit 1
|
||||
# shellcheck disable=SC2086
|
||||
exec docker compose restart $services
|
||||
fi
|
||||
exec docker compose restart "$@"
|
||||
;;
|
||||
recreate)
|
||||
if [ "${1:-}" = "--except" ]; then
|
||||
shift
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") recreate --except <svc>..." >&2; exit 1; }
|
||||
services=$(services_except "$@") || exit 1
|
||||
# shellcheck disable=SC2086
|
||||
exec docker compose up -d --wait --force-recreate --no-deps $services
|
||||
fi
|
||||
if [ $# -eq 0 ]; then
|
||||
docker compose down
|
||||
exec docker compose up -d --wait
|
||||
fi
|
||||
# Single-service recreate: force-recreate the named services only,
|
||||
# leave their dependencies running.
|
||||
exec docker compose up -d --wait --force-recreate --no-deps "$@"
|
||||
;;
|
||||
status|ps)
|
||||
exec docker compose ps "$@"
|
||||
;;
|
||||
logs)
|
||||
exec docker compose logs -f "$@"
|
||||
;;
|
||||
inspect)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") inspect <service> [docker-inspect-args]" >&2; exit 1; }
|
||||
svc="$1"; shift
|
||||
cid=$(docker compose ps -q "$svc")
|
||||
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
|
||||
exec docker inspect "$cid" "$@"
|
||||
;;
|
||||
printenv)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") printenv <service>" >&2; exit 1; }
|
||||
svc="$1"
|
||||
cid=$(docker compose ps -q "$svc")
|
||||
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
|
||||
exec docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$cid"
|
||||
;;
|
||||
pull)
|
||||
exec docker compose pull "$@"
|
||||
;;
|
||||
compose-config)
|
||||
exec docker compose config "$@"
|
||||
;;
|
||||
config)
|
||||
sub="${1:-show}"
|
||||
[ "$#" -gt 0 ] && shift
|
||||
current=$(read_compose_file)
|
||||
case "$sub" in
|
||||
show)
|
||||
print_config "$current"
|
||||
;;
|
||||
add)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config add <name>..." >&2; exit 1; }
|
||||
new_value="${current:-docker-compose.yml}"
|
||||
changed=false
|
||||
for arg in "$@"; do
|
||||
file=$(normalize_override "$arg") || exit 1
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "ERROR: $file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
case ":$new_value:" in
|
||||
*":$file:"*) echo "Already present: $file" ;;
|
||||
*) new_value="$new_value:$file"; changed=true ;;
|
||||
esac
|
||||
done
|
||||
[ "$changed" = true ] && write_compose_file "$new_value"
|
||||
print_config "$new_value"
|
||||
;;
|
||||
remove|rm)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config remove <name>..." >&2; exit 1; }
|
||||
new_value="${current:-docker-compose.yml}"
|
||||
changed=false
|
||||
for arg in "$@"; do
|
||||
file=$(normalize_override "$arg") || exit 1
|
||||
case ":$new_value:" in
|
||||
*":$file:"*)
|
||||
# Drop $file by rebuilding the colon list
|
||||
tmp=""
|
||||
OLD_IFS=$IFS
|
||||
IFS=:
|
||||
for tok in $new_value; do
|
||||
[ "$tok" = "$file" ] || tmp="${tmp:+$tmp:}$tok"
|
||||
done
|
||||
IFS=$OLD_IFS
|
||||
new_value="$tmp"
|
||||
changed=true
|
||||
;;
|
||||
*) echo "Not present: $file" ;;
|
||||
esac
|
||||
done
|
||||
[ "$changed" = true ] && write_compose_file "$new_value"
|
||||
print_config "$new_value"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown config subcommand: $sub" >&2
|
||||
echo "Use: config | config add <name>... | config remove <name>..." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
secrets)
|
||||
if [ ! -f .env ]; then
|
||||
echo "ERROR: .env not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
for var in POSTGRES_PASSWORD DASHBOARD_PASSWORD \
|
||||
SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY \
|
||||
S3_PROTOCOL_ACCESS_KEY_ID S3_PROTOCOL_ACCESS_KEY_SECRET; do
|
||||
line=$(grep "^${var}=" .env | head -n1)
|
||||
if [ -n "$line" ]; then
|
||||
echo "$line"
|
||||
else
|
||||
echo "${var}="
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
;;
|
||||
help|-h|--help)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command>
|
||||
|
||||
Commands:
|
||||
start Start the stack (docker compose up -d --wait)
|
||||
stop Stop the stack (docker compose down)
|
||||
restart [service] Restart the stack (or named services)
|
||||
restart --except <svc>...
|
||||
Restart all services except the named ones
|
||||
recreate [service] Stop then start, or force-recreate one service (--no-deps)
|
||||
recreate --except <svc>...
|
||||
Force-recreate all services except the named ones (--no-deps)
|
||||
status Show service status
|
||||
logs [service] Follow logs (optionally for a single service)
|
||||
inspect <service> Inspect a service's container (forwards extra args to docker inspect)
|
||||
printenv <service> Print a service's environment variables (one per line)
|
||||
pull Pull all images
|
||||
config Show the active COMPOSE_FILE list
|
||||
config add <name> Add an override to COMPOSE_FILE in .env (short name or full filename)
|
||||
config remove <name> Remove an override from COMPOSE_FILE in .env
|
||||
compose-config Dump the fully-resolved docker compose config
|
||||
secrets Show key passwords and API keys from .env
|
||||
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $CMD" >&2
|
||||
echo "Run '$0 help' for usage." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
341
setup.sh
Executable file
341
setup.sh
Executable file
@@ -0,0 +1,341 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Bootstrap a self-hosted Supabase project on Linux (Debian/Ubuntu or RHEL/CentOS/Fedora).
|
||||
#
|
||||
# What it does:
|
||||
# 1. Installs prerequisites: git, openssl, jq, ca-certificates
|
||||
# 2. Installs Docker Engine + Compose plugin (if missing)
|
||||
# 3. Optionally installs the AWS CLI v2 (--with-aws)
|
||||
# 4. Sparse-clones the repo to extract the contents of ./docker
|
||||
# 5. Creates a project directory in CWD and copies docker/* into it
|
||||
# 6. Prompts for the main URLs and writes them to .env
|
||||
# 7. Generates secrets and asymmetric API keys via utils/*.sh
|
||||
#
|
||||
# Usage:
|
||||
# sh setup.sh # interactive
|
||||
# sh setup.sh -y # accept defaults, no prompts
|
||||
# sh setup.sh --project-dir my-supabase # name the project directory
|
||||
# sh setup.sh --skip-deps # skip system-package installation
|
||||
# sh setup.sh --with-aws # also install the AWS CLI v2
|
||||
#
|
||||
# curl -fsSL <url-to-this-script> | sh # bootstrap from scratch in CWD
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_DIR="supabase-project"
|
||||
SKIP_DEPS=0
|
||||
WITH_AWS=0
|
||||
ASSUME_YES=0
|
||||
|
||||
print_help() {
|
||||
cat <<EOF
|
||||
Usage: setup.sh [options]
|
||||
|
||||
Options:
|
||||
-p, --project-dir <name> Name of the project directory (default: supabase-project)
|
||||
--skip-deps Skip installation of system packages
|
||||
--with-aws Install the AWS CLI v2
|
||||
-y, --yes Non-interactive: accept defaults, no prompts
|
||||
-h, --help Show this help and exit
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-p|--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--skip-deps) SKIP_DEPS=1; shift ;;
|
||||
--with-aws) WITH_AWS=1; shift ;;
|
||||
-y|--yes) ASSUME_YES=1; shift ;;
|
||||
-h|--help) print_help; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; print_help; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
SUDO=""
|
||||
else
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
log() { printf "===> %s\n" "$*"; }
|
||||
warn() { printf "WARNING: %s\n" "$*" >&2; }
|
||||
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
# Prompt with a default; echoes the chosen value on stdout.
|
||||
# Reads from /dev/tty so prompts work even when stdin is a pipe (curl | sh).
|
||||
# Falls back to the default with -y or when no controlling terminal exists.
|
||||
ask() {
|
||||
# ask <prompt> <default> -> echoes chosen value
|
||||
if [ "$ASSUME_YES" = "1" ] || ! { : > /dev/tty; } 2>/dev/null; then
|
||||
printf '%s' "$2"
|
||||
return
|
||||
fi
|
||||
printf "%s [%s]: " "$1" "$2" > /dev/tty
|
||||
read -r reply < /dev/tty
|
||||
[ -z "$reply" ] && reply="$2"
|
||||
printf '%s' "$reply"
|
||||
}
|
||||
|
||||
OS_FAMILY=""
|
||||
OS_ID=""
|
||||
OS_CODENAME=""
|
||||
|
||||
detect_os() {
|
||||
[ -f /etc/os-release ] || die "Cannot detect OS: /etc/os-release missing. Linux only."
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
OS_ID="$ID"
|
||||
OS_CODENAME="${VERSION_CODENAME:-}"
|
||||
case "$ID" in
|
||||
ubuntu|debian) OS_FAMILY="debian" ;;
|
||||
centos|rhel|fedora|rocky|almalinux|ol|amzn) OS_FAMILY="rhel" ;;
|
||||
*)
|
||||
case "${ID_LIKE:-}" in
|
||||
*debian*|*ubuntu*) OS_FAMILY="debian" ;;
|
||||
*rhel*|*fedora*|*centos*) OS_FAMILY="rhel" ;;
|
||||
*) die "Unsupported distribution: $ID" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
log "Detected OS: $ID ($OS_FAMILY)"
|
||||
}
|
||||
|
||||
pkg_update() {
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
$SUDO apt-get update -qq -y
|
||||
else
|
||||
$SUDO dnf makecache -q -y || true
|
||||
fi
|
||||
}
|
||||
|
||||
pkg_install() {
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
$SUDO apt-get install -qq -y "$@"
|
||||
else
|
||||
$SUDO dnf install -q -y "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_base_packages() {
|
||||
log "Installing base packages: git, openssl, jq, ca-certificates"
|
||||
pkg_update
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
pkg_install git openssl jq ca-certificates \
|
||||
apt-transport-https gnupg lsb-release
|
||||
else
|
||||
pkg_install git openssl jq ca-certificates dnf-plugins-core
|
||||
fi
|
||||
}
|
||||
|
||||
docker_present() {
|
||||
command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if docker_present; then
|
||||
log "Docker already installed: $(docker --version)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Installing Docker Engine and Compose plugin"
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
$SUDO install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" \
|
||||
| $SUDO gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
|
||||
$SUDO chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
codename="${OS_CODENAME:-$(lsb_release -cs 2>/dev/null || echo stable)}"
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS_ID} ${codename} stable" \
|
||||
| $SUDO tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
$SUDO apt-get update -y
|
||||
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
else
|
||||
# Amazon Linux
|
||||
if [ "$OS_ID" = "amzn" ]; then
|
||||
# Install Docker from the repo
|
||||
pkg_install docker
|
||||
# Install Docker Compose
|
||||
$SUDO mkdir -p /usr/local/lib/docker/cli-plugins && \
|
||||
$SUDO curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \
|
||||
-o /usr/local/lib/docker/cli-plugins/docker-compose && \
|
||||
$SUDO chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
else
|
||||
repo_distro="centos"
|
||||
case "$OS_ID" in
|
||||
fedora) repo_distro="fedora" ;;
|
||||
rhel) repo_distro="rhel" ;;
|
||||
esac
|
||||
$SUDO dnf config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" 2>/dev/null \
|
||||
|| $SUDO dnf-3 config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo"
|
||||
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Enabling and starting docker service"
|
||||
$SUDO systemctl enable --now docker || warn "Could not enable docker via systemctl; start it manually."
|
||||
|
||||
docker_present || die "Docker installation finished but 'docker compose' is still unavailable."
|
||||
}
|
||||
|
||||
install_aws() {
|
||||
if command -v aws >/dev/null 2>&1; then
|
||||
log "AWS CLI already installed: $(aws --version 2>&1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64|amd64) aws_arch="x86_64" ;;
|
||||
aarch64|arm64) aws_arch="aarch64" ;;
|
||||
*) die "Unsupported architecture for AWS CLI: $arch" ;;
|
||||
esac
|
||||
|
||||
log "Installing AWS CLI v2 (${aws_arch})"
|
||||
command -v unzip >/dev/null 2>&1 || pkg_install unzip
|
||||
tmp=$(mktemp -d)
|
||||
(
|
||||
cd "$tmp"
|
||||
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${aws_arch}.zip" -o awscliv2.zip
|
||||
unzip -q -o awscliv2.zip
|
||||
$SUDO ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
|
||||
)
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
SRC_DIR=""
|
||||
SRC_TMP=""
|
||||
|
||||
prepare_source() {
|
||||
log "Sparse-cloning supabase repo"
|
||||
SRC_TMP=$(mktemp -d) || return 1
|
||||
git clone --filter=blob:none --no-checkout --depth=1 --quiet \
|
||||
https://github.com/supabase/supabase "$SRC_TMP/supabase" 2>/dev/null || \
|
||||
{ rm -rf "$SRC_TMP"; return 1; }
|
||||
|
||||
cd "$SRC_TMP/supabase" || { rm -rf "$SRC_TMP"; return 1; }
|
||||
git sparse-checkout init --cone && \
|
||||
git sparse-checkout set docker && \
|
||||
git checkout --quiet 2>/dev/null
|
||||
SRC_DIR="$PWD/docker"
|
||||
cd - > /dev/null
|
||||
}
|
||||
|
||||
cleanup_src_tmp() {
|
||||
if [ -n "$SRC_TMP" ] && [ -d "$SRC_TMP" ]; then
|
||||
rm -rf "$SRC_TMP"
|
||||
fi
|
||||
}
|
||||
trap cleanup_src_tmp EXIT
|
||||
|
||||
read_env() {
|
||||
grep "^$1=" .env 2>/dev/null | head -n1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
log "Setup starting in $(pwd)"
|
||||
log "This may take several minutes..."
|
||||
|
||||
if [ "$SKIP_DEPS" = "1" ]; then
|
||||
log "Skipping system-package installation (--skip-deps)"
|
||||
else
|
||||
detect_os
|
||||
install_base_packages
|
||||
install_docker
|
||||
fi
|
||||
|
||||
if [ "$WITH_AWS" = "1" ]; then
|
||||
install_aws
|
||||
fi
|
||||
|
||||
# Idempotent re-run: if CWD is already a set-up project, skip bootstrap.
|
||||
# A clone has docker-compose.yml + utils/ but only .env.example;
|
||||
# a set-up project also has a real .env.
|
||||
if [ -f .env ] && [ -f docker-compose.yml ] && [ -d utils ]; then
|
||||
log "Already in a Supabase project directory; skipping bootstrap."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
prepare_source
|
||||
|
||||
target="$(pwd)/$PROJECT_DIR"
|
||||
if [ -e "$target" ]; then
|
||||
die "Target $target already exists. Pick a different name with --project-dir"
|
||||
fi
|
||||
|
||||
log "Creating project at $target"
|
||||
mkdir -p "$target"
|
||||
cp -rf "$SRC_DIR/." "$target/"
|
||||
if [ -f "$target/.env.example" ] && [ ! -f "$target/.env" ]; then
|
||||
cp "$target/.env.example" "$target/.env"
|
||||
fi
|
||||
|
||||
cd "$target"
|
||||
|
||||
current_public_url=$(read_env SUPABASE_PUBLIC_URL)
|
||||
current_api_url=$(read_env API_EXTERNAL_URL)
|
||||
current_site_url=$(read_env SITE_URL)
|
||||
|
||||
[ -z "$current_public_url" ] && current_public_url="http://localhost:8000"
|
||||
[ -z "$current_api_url" ] && current_api_url="$current_public_url"
|
||||
[ -z "$current_site_url" ] && current_site_url="http://localhost:3000"
|
||||
|
||||
if [ "$ASSUME_YES" = "1" ] || ! { : > /dev/tty; } 2>/dev/null; then
|
||||
log "Non-interactive: keeping default URLs (edit .env to change)"
|
||||
else
|
||||
echo ""
|
||||
echo "Configure the main URLs (press Enter to accept the default)."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
public_url=$(ask "SUPABASE_PUBLIC_URL (Studio + APIs)" "$current_public_url")
|
||||
api_url=$(ask "API_EXTERNAL_URL (Auth callbacks)" "$public_url")
|
||||
site_url=$(ask "SITE_URL (default Auth redirect)" "$current_site_url")
|
||||
|
||||
# Suggest PROXY_DOMAIN from the public_url host (unless it's localhost-ish)
|
||||
public_host=$(printf '%s' "$public_url" | sed -e 's|^https*://||' -e 's|/.*$||' -e 's|:.*$||')
|
||||
case "$public_host" in
|
||||
localhost|127.*|"") current_proxy_domain=$(read_env PROXY_DOMAIN) ;;
|
||||
*) current_proxy_domain="$public_host" ;;
|
||||
esac
|
||||
[ -z "$current_proxy_domain" ] && current_proxy_domain="your-domain.example.com"
|
||||
|
||||
proxy_domain=$(ask "PROXY_DOMAIN (for nginx/caddy HTTPS proxy)" "$current_proxy_domain")
|
||||
|
||||
# Derive CERTBOT_EMAIL = admin@<last-two-labels-of-proxy-domain>.
|
||||
# Naive: doesn't handle ccTLDs like .co.uk; user can edit .env after.
|
||||
domain_root=$(printf '%s' "$proxy_domain" | awk -F. 'NF>=2 { print $(NF-1)"."$NF; next } { print }')
|
||||
certbot_email="admin@${domain_root}"
|
||||
log "Setting CERTBOT_EMAIL=${certbot_email}"
|
||||
|
||||
sed -i.old \
|
||||
-e "s|^SUPABASE_PUBLIC_URL=.*$|SUPABASE_PUBLIC_URL=${public_url}|" \
|
||||
-e "s|^API_EXTERNAL_URL=.*$|API_EXTERNAL_URL=${api_url}|" \
|
||||
-e "s|^SITE_URL=.*$|SITE_URL=${site_url}|" \
|
||||
-e "s|^PROXY_DOMAIN=.*$|PROXY_DOMAIN=${proxy_domain}|" \
|
||||
-e "s|^CERTBOT_EMAIL=.*$|CERTBOT_EMAIL=${certbot_email}|" \
|
||||
.env
|
||||
rm -f .env.old
|
||||
|
||||
log "Generating secrets and legacy API keys"
|
||||
sh utils/generate-keys.sh --update-env
|
||||
|
||||
log "Generating asymmetric key pair and opaque API keys"
|
||||
sh utils/add-new-auth-keys.sh --update-env
|
||||
|
||||
log "Pulling Docker images"
|
||||
docker compose pull || warn "docker compose pull failed; you can retry later."
|
||||
|
||||
echo ""
|
||||
echo "Setup complete. Project ready at: $(pwd)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " cd $(pwd)"
|
||||
echo " sh run.sh config"
|
||||
echo " sh run.sh secrets"
|
||||
echo " sh run.sh start"
|
||||
echo ""
|
||||
echo "To enable docker-compose overrides (pg17, envoy, caddy, nginx, rustfs, s3, logs):"
|
||||
echo " sh run.sh config add pg17"
|
||||
echo ""
|
||||
11
tests/docker-compose.rustfs.test.yml
Normal file
11
tests/docker-compose.rustfs.test.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Test override: exposes the S3 backend port for direct testing.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.rustfs.yml \
|
||||
# -f ./tests/docker-compose.rustfs.test.yml up -d
|
||||
#
|
||||
|
||||
services:
|
||||
rustfs:
|
||||
ports:
|
||||
- "${S3_BACKEND_TEST_PORT:-9100}:9000"
|
||||
13
tests/docker-compose.s3.test.yml
Normal file
13
tests/docker-compose.s3.test.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Test override: exposes the S3 backend port for direct testing.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml \
|
||||
# -f ./tests/docker-compose.s3.test.yml up -d
|
||||
#
|
||||
# When swapping to a different S3 backend (e.g. RustFS), update the
|
||||
# service name and internal port to match the new backend.
|
||||
|
||||
services:
|
||||
minio:
|
||||
ports:
|
||||
- "${S3_BACKEND_TEST_PORT:-9100}:9000"
|
||||
369
tests/test-auth-keys.sh
Normal file
369
tests/test-auth-keys.sh
Normal file
@@ -0,0 +1,369 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test API key types and asymmetric auth against a running self-hosted instance.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-auth-keys.sh # Uses http://localhost:8000
|
||||
# sh test-auth-keys.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance
|
||||
# - .env file with all keys configured
|
||||
# - jq (for JSON parsing)
|
||||
# - node >= 16 (for HS256 token minting test only)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in jq node; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read keys from .env
|
||||
JWT_SECRET=$(grep '^JWT_SECRET=' .env | cut -d= -f2-)
|
||||
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' .env | cut -d= -f2-)
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name (HTTP $actual)"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
http_status() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Testing against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. Route tests with API key types
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- REST API (/rest/v1/) ---"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $ANON_KEY")"
|
||||
check "Legacy SERVICE_ROLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SERVICE_ROLE_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
check "New SECRET_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_SECRET_KEY")"
|
||||
else
|
||||
echo " SKIP: Opaque keys not configured"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/")"
|
||||
check "Invalid key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: invalid-key")"
|
||||
|
||||
echo ""
|
||||
echo "--- Auth (/auth/v1/settings) ---"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings")"
|
||||
|
||||
echo ""
|
||||
echo "--- Storage (/storage/v1/bucket) ---"
|
||||
# Storage has no key-auth - passes through, Storage returns its own errors
|
||||
check "No key -> not 401 (Storage handles auth)" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/storage/v1/bucket")" != "401" ] && echo true || echo false)"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# With opaque key, Kong translates to asymmetric JWT in Authorization
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Storage S3 (/storage/v1/s3/) ---"
|
||||
# S3 uses AWS SigV4 auth (not apikey) - the request-transformer Lua expression
|
||||
# passes the Authorization header through unchanged for non-sb_ values
|
||||
check "S3 route accessible" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/storage/v1/s3/")" != "502" ] && echo true || echo false)"
|
||||
|
||||
echo ""
|
||||
echo "--- GraphQL (/graphql/v1) ---"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime REST (/realtime/v1/api/) ---"
|
||||
# Realtime REST API - expect 200 or other non-401 response with valid key
|
||||
check "Legacy ANON_KEY -> not 401" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY -> not 401" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")" != "401" ] && echo true || echo false)"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants")"
|
||||
|
||||
echo ""
|
||||
echo "--- supabase-js style requests (apikey + Authorization) ---"
|
||||
# supabase-js sends both apikey header AND Authorization: Bearer <apikey>
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "apikey + Authorization: Bearer sb_ (replace path)" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
check "Legacy apikey + Authorization: Bearer <legacy jwt>" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $ANON_KEY")"
|
||||
|
||||
echo ""
|
||||
echo "--- Edge cases ---"
|
||||
# Opaque key in Authorization only (no apikey header) - should be rejected by key-auth
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "sb_ in Authorization only (no apikey) -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- JWKS endpoint ---"
|
||||
check "JWKS public endpoint (no auth)" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/.well-known/jwks.json")"
|
||||
|
||||
# Verify JWKS content: should have EC key, should NOT have symmetric key
|
||||
jwks_content=$(curl -s "$BASE_URL/auth/v1/.well-known/jwks.json")
|
||||
jwks_has_ec=$(echo "$jwks_content" | jq -r '[.keys[] | .kty] | if any(. == "EC") then "true" else "false" end' 2>/dev/null)
|
||||
jwks_has_oct=$(echo "$jwks_content" | jq -r '[.keys[] | .kty] | if any(. == "oct") then "true" else "false" end' 2>/dev/null)
|
||||
check "JWKS contains EC public key" "true" "$jwks_has_ec"
|
||||
check "JWKS does NOT contain symmetric key" "false" "$jwks_has_oct"
|
||||
|
||||
#echo ""
|
||||
#echo "--- OAuth metadata endpoint ---"
|
||||
#check "well-known oauth (no auth)" "200" \
|
||||
# "$(http_status "$BASE_URL/.well-known/oauth-authorization-server")"
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime WebSocket upgrade ---"
|
||||
# Test that WebSocket upgrade request gets through (expect 101 or non-401)
|
||||
# curl --max-time to prevent hanging on successful upgrade (101 keeps connection open)
|
||||
ws_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
|
||||
"$BASE_URL/realtime/v1/websocket?apikey=$ANON_KEY&vsn=1.0.0" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
|
||||
# 101 = upgrade success, 000 = timeout (connection stayed open = success)
|
||||
check "WebSocket upgrade with legacy key -> not 401" "true" \
|
||||
"$([ "$ws_status" != "401" ] && echo true || echo false)"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
ws_status_new=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
|
||||
"$BASE_URL/realtime/v1/websocket?apikey=$SUPABASE_PUBLISHABLE_KEY&vsn=1.0.0" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
|
||||
check "WebSocket upgrade with opaque key -> not 401" "true" \
|
||||
"$([ "$ws_status_new" != "401" ] && echo true || echo false)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. User session JWT tests
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- User session JWT ---"
|
||||
|
||||
# Create user via admin API (works regardless of email autoconfirm setting)
|
||||
test_email="test-keys-$$@example.com"
|
||||
test_password="test-password-123456"
|
||||
|
||||
create_resp=$(curl -s "$BASE_URL/auth/v1/admin/users" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\",\"email_confirm\":true}")
|
||||
|
||||
test_user_id=$(echo "$create_resp" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
# Sign in to get session JWT
|
||||
auth_response=$(curl -s "$BASE_URL/auth/v1/token?grant_type=password" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
access_token=$(echo "$auth_response" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$access_token" ]; then
|
||||
# Check the algorithm in the JWT header
|
||||
jwt_alg=$(echo "$access_token" | cut -d. -f1 | \
|
||||
jq -Rr '@base64d | fromjson | .alg // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$jwt_alg" ]; then
|
||||
echo " INFO: User session JWT signed with: $jwt_alg"
|
||||
if [ "$jwt_alg" = "ES256" ]; then
|
||||
check "JWT uses ES256 (asymmetric)" "ES256" "$jwt_alg"
|
||||
else
|
||||
check "JWT uses HS256 (legacy)" "HS256" "$jwt_alg"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use the session JWT with PostgREST
|
||||
check "Session JWT with PostgREST" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
|
||||
# Use the session JWT with Storage
|
||||
check "Session JWT with Storage" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
|
||||
# CRITICAL: Authenticated user + opaque key (most common supabase-js flow)
|
||||
# supabase-js sends apikey: sb_publishable_xxx AND Authorization: Bearer <user_session_jwt>
|
||||
# The expression MUST keep the user JWT and NOT replace it with the anon asymmetric JWT
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
echo ""
|
||||
echo "--- Authenticated user + opaque key (critical path) ---"
|
||||
check "Opaque apikey + user JWT -> PostgREST uses user JWT" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
check "Opaque apikey + user JWT -> Storage uses user JWT" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
check "Opaque apikey + user JWT -> Auth uses user JWT" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/user" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
fi
|
||||
else
|
||||
check "Sign in test user" "true" "false"
|
||||
fi
|
||||
|
||||
# Clean up test user
|
||||
if [ -n "$test_user_id" ]; then
|
||||
curl -s -o /dev/null "$BASE_URL/auth/v1/admin/users/$test_user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. HS256 backward compatibility
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- HS256 backward compatibility ---"
|
||||
|
||||
# Mint a legacy HS256 JWT with role=anon (simulating a pre-migration token)
|
||||
hs256_token=$(JWT_SECRET="$JWT_SECRET" node -e "
|
||||
const crypto = require('crypto');
|
||||
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
|
||||
const payload = Buffer.from(JSON.stringify({
|
||||
role:'anon',iss:'supabase',
|
||||
iat:Math.floor(Date.now()/1000),
|
||||
exp:Math.floor(Date.now()/1000)+3600
|
||||
})).toString('base64url');
|
||||
const sig = crypto.createHmac('sha256',process.env.JWT_SECRET)
|
||||
.update(header+'.'+payload).digest('base64url');
|
||||
console.log(header+'.'+payload+'.'+sig);
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -n "$hs256_token" ]; then
|
||||
check "HS256 token with PostgREST (backward compat)" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $hs256_token")"
|
||||
else
|
||||
echo " SKIP: Could not mint HS256 token (node required)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. JWT_KEYS format validation
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- JWT_KEYS format ---"
|
||||
|
||||
JWT_KEYS_VAL=$(grep '^JWT_KEYS=' .env | cut -d= -f2-)
|
||||
if [ -n "$JWT_KEYS_VAL" ]; then
|
||||
# Auth expects a JSON array, not a JWKS object
|
||||
jwt_keys_is_array=$(echo "$JWT_KEYS_VAL" | jq -r 'if type == "array" then "true" else "false" end' 2>/dev/null)
|
||||
check "JWT_KEYS is JSON array (not JWKS object)" "true" "$jwt_keys_is_array"
|
||||
|
||||
jwt_keys_has_sign=$(echo "$JWT_KEYS_VAL" | jq -r 'if any(.[]; .key_ops and (.key_ops | index("sign"))) then "true" else "false" end' 2>/dev/null)
|
||||
check "JWT_KEYS has a signing key (key_ops: sign)" "true" "$jwt_keys_has_sign"
|
||||
else
|
||||
echo " SKIP: JWT_KEYS not configured"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
165
tests/test-container-logs.sh
Normal file
165
tests/test-container-logs.sh
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Verify all self-hosted Supabase services started correctly by checking log output.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-container-logs.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance (docker compose up)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
project_name="${COMPOSE_PROJECT_NAME:-supabase}"
|
||||
|
||||
fail_msg() {
|
||||
fail=$((fail + 1))
|
||||
echo " FAIL: $1"
|
||||
}
|
||||
|
||||
pass_msg() {
|
||||
pass=$((pass + 1))
|
||||
echo " PASS: $1"
|
||||
}
|
||||
|
||||
# The `docker ps` fallback in the helpers below exists because the script
|
||||
# doesn't know which compose `-f` flags the user ran `up` with. `docker compose
|
||||
# ps` only sees services defined in the currently loaded compose files, but
|
||||
# compose stamps `com.docker.compose.{project,service}` labels at `up` time -
|
||||
# so a label-based lookup finds the container regardless of which override
|
||||
# files are active in this shell.
|
||||
|
||||
is_service_running() {
|
||||
service="$1"
|
||||
if docker compose ps --services --status running 2>/dev/null | grep -q "^$service$"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker ps --filter "label=com.docker.compose.project=$project_name" \
|
||||
--filter "label=com.docker.compose.service=$service" \
|
||||
--filter "status=running" \
|
||||
--quiet | grep -q '.'
|
||||
}
|
||||
|
||||
get_container_id() {
|
||||
service="$1"
|
||||
|
||||
container_id=$(docker compose ps -q "$service" 2>/dev/null || true)
|
||||
if [ -n "$container_id" ]; then
|
||||
printf '%s' "$container_id"
|
||||
return
|
||||
fi
|
||||
|
||||
container_id=$(docker ps -a \
|
||||
--filter "label=com.docker.compose.project=$project_name" \
|
||||
--filter "label=com.docker.compose.service=$service" \
|
||||
--quiet)
|
||||
|
||||
set -- $container_id
|
||||
printf '%s' "$1"
|
||||
}
|
||||
|
||||
# Check that a service's logs contain all expected patterns
|
||||
check_logs() {
|
||||
service="$1"
|
||||
shift
|
||||
|
||||
logs=$(docker compose logs "$service" 2>/dev/null || true)
|
||||
if [ -z "$logs" ]; then
|
||||
container_id=$(get_container_id "$service")
|
||||
if [ -n "$container_id" ]; then
|
||||
logs=$(docker logs "$container_id" 2>&1 || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$logs" ]; then
|
||||
fail_msg "$service (no logs found)"
|
||||
return
|
||||
fi
|
||||
|
||||
for pattern in "$@"; do
|
||||
if ! echo "$logs" | grep -q -i -E "$pattern"; then
|
||||
fail_msg "$service (missing: $pattern)"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
pass_msg "$service"
|
||||
}
|
||||
|
||||
check_logs_if_running() {
|
||||
service="$1"
|
||||
shift
|
||||
|
||||
if is_service_running "$service"; then
|
||||
check_logs "$service" "$@"
|
||||
else
|
||||
pass_msg "$service (skipped: service not running)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Checking service startup logs ==="
|
||||
echo ""
|
||||
|
||||
check_logs db \
|
||||
'PostgreSQL init process complete; ready for start up.|Skipping initialization'
|
||||
|
||||
check_logs auth \
|
||||
'db worker started'
|
||||
|
||||
check_logs_if_running kong \
|
||||
'init.lua.*declarative config loaded'
|
||||
|
||||
check_logs_if_running api-gw \
|
||||
'Envoy configuration generated successfully' \
|
||||
'Starting Envoy...'
|
||||
|
||||
check_logs rest \
|
||||
'Schema cache loaded in.*milliseconds'
|
||||
|
||||
check_logs realtime \
|
||||
'Starting Realtime' \
|
||||
'Connected to Postgres database' \
|
||||
'Janitor started' \
|
||||
'Starting MetricsCleaner'
|
||||
|
||||
check_logs storage \
|
||||
'Started Successfully'
|
||||
|
||||
check_logs studio \
|
||||
'ready in.*s$'
|
||||
|
||||
check_logs meta \
|
||||
'Server listening at http'
|
||||
|
||||
check_logs functions \
|
||||
'main function started'
|
||||
|
||||
check_logs_if_running analytics \
|
||||
'Access LogflareWeb.Endpoint at http://localhost:4000' \
|
||||
'Executing startup tasks' \
|
||||
'Ensuring single tenant user is seeded'
|
||||
|
||||
check_logs supavisor \
|
||||
'Connected to Postgres database' \
|
||||
'HEAD /api/health$'
|
||||
|
||||
check_logs_if_running vector \
|
||||
'Vector has started'
|
||||
|
||||
check_logs imgproxy \
|
||||
'Starting server at :5001'
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo "Inspect logs: docker compose logs <service>"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
239
tests/test-pg17-upgrade.sh
Normal file
239
tests/test-pg17-upgrade.sh
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test Postgres 15 -> 17 upgrade for self-hosted Supabase.
|
||||
#
|
||||
# Seeds test data on a running Postgres 15 stack, runs the upgrade script,
|
||||
# and verifies data integrity + service connectivity using pgTAP.
|
||||
#
|
||||
# Usage:
|
||||
# cd docker/
|
||||
# sudo bash tests/test-pg17-upgrade.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase with a clean, tests-only Postgres 15:
|
||||
# docker compose up -d
|
||||
# - .env file with POSTGRES_PASSWORD, ANON_KEY
|
||||
#
|
||||
|
||||
set -eu
|
||||
|
||||
DB_CONTAINER="supabase-db"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the docker/ directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pg_password=$(grep '^POSTGRES_PASSWORD=' .env | cut -d '=' -f 2-)
|
||||
anon_key=$(grep '^ANON_KEY=' .env | cut -d '=' -f 2- || true)
|
||||
|
||||
if [ -z "$pg_password" ]; then
|
||||
echo "Error: POSTGRES_PASSWORD not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_sql() {
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 "$@"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Postgres 15 -> 17 Upgrade Test ==="
|
||||
echo ""
|
||||
|
||||
# --- Verify we're starting from Postgres 15 --------------------------------
|
||||
|
||||
current_version=$(run_sql -A -t -c "SHOW server_version;" | head -1)
|
||||
case "$current_version" in
|
||||
15.*) echo "Starting version: PostgreSQL $current_version" ;;
|
||||
17.*) echo "Error: Already on Postgres 17. Start with a PG15 stack."; exit 1 ;;
|
||||
*) echo "Error: Unexpected version: $current_version"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# --- Seed test data --------------------------------------------------------
|
||||
# Note: this script is designed to run against a fresh docker-compose stack,
|
||||
# not an existing database with user data.
|
||||
|
||||
echo ""
|
||||
echo "Seeding test data on Postgres 15..."
|
||||
|
||||
run_sql <<'EOSQL'
|
||||
-- Test table with various column types
|
||||
CREATE TABLE IF NOT EXISTS public._upgrade_test (
|
||||
id serial PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
value numeric(10,2),
|
||||
created_at timestamptz DEFAULT now(),
|
||||
metadata jsonb
|
||||
);
|
||||
|
||||
TRUNCATE public._upgrade_test;
|
||||
INSERT INTO public._upgrade_test (name, value, metadata) VALUES
|
||||
('alpha', 1.50, '{"tag": "a"}'),
|
||||
('bravo', 2.75, '{"tag": "b"}'),
|
||||
('charlie', 3.00, '{"tag": "c"}'),
|
||||
('delta', 4.25, '{"tag": "d"}'),
|
||||
('echo', 5.99, '{"tag": "e"}');
|
||||
|
||||
-- Index
|
||||
CREATE INDEX IF NOT EXISTS _upgrade_test_name_idx ON public._upgrade_test (name);
|
||||
|
||||
-- Function
|
||||
CREATE OR REPLACE FUNCTION public._upgrade_test_fn(n int)
|
||||
RETURNS int LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT n * 2;
|
||||
$$;
|
||||
|
||||
-- Grant access so PostgREST can read it
|
||||
GRANT SELECT ON public._upgrade_test TO anon, authenticated;
|
||||
EOSQL
|
||||
|
||||
pre_count=$(run_sql -A -t -c "SELECT count(*) FROM public._upgrade_test;" | tr -d '[:space:]')
|
||||
pre_checksum=$(run_sql -A -t -c "SELECT md5(string_agg(name || value::text, ',' ORDER BY id)) FROM public._upgrade_test;" | tr -d '[:space:]')
|
||||
|
||||
echo " Rows: $pre_count"
|
||||
echo " Checksum: $pre_checksum"
|
||||
|
||||
# --- Run upgrade -----------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Running upgrade script..."
|
||||
echo ""
|
||||
|
||||
bash utils/upgrade-pg17.sh --yes
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Verify with pgTAP ----------------------------------------------------
|
||||
|
||||
echo "Running pgTAP verification..."
|
||||
echo ""
|
||||
|
||||
# Use a non-quoted heredoc so $pre_count and $pre_checksum are interpolated
|
||||
run_sql <<EOSQL
|
||||
CREATE EXTENSION IF NOT EXISTS pgtap;
|
||||
|
||||
SELECT plan(11);
|
||||
|
||||
-- Version
|
||||
SELECT ok(version() LIKE 'PostgreSQL 17%', 'Running Postgres 17');
|
||||
|
||||
-- Table
|
||||
SELECT has_table('public', '_upgrade_test', 'Test table survived upgrade');
|
||||
|
||||
-- Row count
|
||||
SELECT is(
|
||||
(SELECT count(*)::int FROM public._upgrade_test),
|
||||
${pre_count},
|
||||
'Row count preserved'
|
||||
);
|
||||
|
||||
-- Data checksum
|
||||
SELECT is(
|
||||
(SELECT md5(string_agg(name || value::text, ',' ORDER BY id)) FROM public._upgrade_test),
|
||||
'${pre_checksum}',
|
||||
'Data checksum matches'
|
||||
);
|
||||
|
||||
-- Index
|
||||
SELECT has_index('public', '_upgrade_test', '_upgrade_test_name_idx', 'Index survived upgrade');
|
||||
|
||||
-- Function
|
||||
SELECT has_function('public', '_upgrade_test_fn', ARRAY['integer'], 'Function survived upgrade');
|
||||
SELECT is(public._upgrade_test_fn(21), 42, 'Function returns correct result');
|
||||
|
||||
-- Core extensions
|
||||
-- Note: pgsodium may not be created as an extension in the postgres database
|
||||
-- on default self-hosted installs (it's loaded via shared_preload_libraries
|
||||
-- but the CREATE EXTENSION is conditional in the init migration).
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net'),
|
||||
'pg_net extension exists'
|
||||
);
|
||||
|
||||
-- Roles
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_etl_admin'),
|
||||
'supabase_etl_admin role exists'
|
||||
);
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_read_only_user'),
|
||||
'supabase_read_only_user role exists'
|
||||
);
|
||||
|
||||
-- postgres is not superuser
|
||||
SELECT ok(
|
||||
NOT (SELECT rolsuper FROM pg_roles WHERE rolname = 'postgres'),
|
||||
'postgres role is not superuser'
|
||||
);
|
||||
|
||||
SELECT * FROM finish(true);
|
||||
EOSQL
|
||||
|
||||
# --- Check service connectivity --------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Checking service connectivity..."
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# PostgREST
|
||||
if [ -n "$anon_key" ]; then
|
||||
rest_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "apikey: $anon_key" \
|
||||
-H "Authorization: Bearer $anon_key" \
|
||||
"http://localhost:8000/rest/v1/_upgrade_test?select=count" 2>/dev/null) || rest_status="000"
|
||||
check "PostgREST connectivity" "200" "$rest_status"
|
||||
fi
|
||||
|
||||
# Auth health (needs apikey header through Kong)
|
||||
if [ -n "$anon_key" ]; then
|
||||
auth_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "apikey: $anon_key" \
|
||||
"http://localhost:8000/auth/v1/health" 2>/dev/null) || auth_status="000"
|
||||
check "Auth service health" "200" "$auth_status"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Services: $pass passed, $fail failed"
|
||||
|
||||
# --- Clean up test artifacts ----------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Cleaning up test artifacts..."
|
||||
|
||||
run_sql <<'EOSQL' || true
|
||||
DROP FUNCTION IF EXISTS public._upgrade_test_fn(int);
|
||||
DROP TABLE IF EXISTS public._upgrade_test;
|
||||
DROP EXTENSION IF EXISTS pgtap;
|
||||
EOSQL
|
||||
|
||||
# --- Summary --------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo "=== SOME TESTS FAILED ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Upgrade test passed ==="
|
||||
echo ""
|
||||
echo "To reclaim disk space:"
|
||||
echo " rm -rf ./volumes/db/data.bak.pg15 ./volumes/db/pg17_upgrade_bin_*.tar.gz"
|
||||
echo ""
|
||||
358
tests/test-s3-backend.sh
Normal file
358
tests/test-s3-backend.sh
Normal file
@@ -0,0 +1,358 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test S3 backend directly, bypassing the Storage service.
|
||||
#
|
||||
# Validates that the S3-compatible backend (MinIO, RustFS, etc.) handles
|
||||
# all S3 operations that Storage relies on. Uses the aws cli so the test
|
||||
# is backend-agnostic — no vendor-specific tools required.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-s3-backend.sh # Uses localhost:9100
|
||||
# sh test-s3-backend.sh <backend_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance with S3 backend + test override:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml \
|
||||
# -f ./tests/docker-compose.s3.test.yml up -d
|
||||
# - .env file with MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, GLOBAL_S3_BUCKET
|
||||
# - aws cli v2
|
||||
# - jq (for JSON parsing)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BACKEND_URL="${1:-http://localhost:${S3_BACKEND_TEST_PORT:-9100}}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in aws jq; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read backend credentials from .env
|
||||
BACKEND_ACCESS_KEY=$(grep '^MINIO_ROOT_USER=' .env | cut -d= -f2-)
|
||||
BACKEND_SECRET_KEY=$(grep '^MINIO_ROOT_PASSWORD=' .env | cut -d= -f2-)
|
||||
GLOBAL_S3_BUCKET=$(grep '^GLOBAL_S3_BUCKET=' .env | cut -d= -f2-)
|
||||
REGION=$(grep '^REGION=' .env | cut -d= -f2-)
|
||||
REGION="${REGION:-us-east-1}"
|
||||
|
||||
if [ -z "$BACKEND_ACCESS_KEY" ] || [ -z "$BACKEND_SECRET_KEY" ]; then
|
||||
echo "Error: MINIO_ROOT_USER or MINIO_ROOT_PASSWORD not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrapper for aws commands against the backend directly
|
||||
s3() {
|
||||
AWS_ACCESS_KEY_ID="$BACKEND_ACCESS_KEY" \
|
||||
AWS_SECRET_ACCESS_KEY="$BACKEND_SECRET_KEY" \
|
||||
aws "$@" --endpoint-url "$BACKEND_URL" --region "$REGION" 2>&1
|
||||
}
|
||||
|
||||
bucket_name="backend-test-$$"
|
||||
|
||||
echo ""
|
||||
echo "=== S3 backend test against $BACKEND_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. ListBuckets (backend reachable)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- Connectivity ---"
|
||||
list_output=$(s3 s3api list-buckets --output json)
|
||||
list_ok=$(echo "$list_output" | jq -r 'if .Buckets then "true" else "false" end' 2>/dev/null)
|
||||
check "Backend reachable (ListBuckets)" "true" "$list_ok"
|
||||
|
||||
if [ "$list_ok" != "true" ]; then
|
||||
echo " Cannot reach backend. Is the test override running?"
|
||||
echo " Response: $list_output"
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. Verify GLOBAL_S3_BUCKET exists
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Storage bucket ---"
|
||||
if [ -n "$GLOBAL_S3_BUCKET" ]; then
|
||||
storage_bucket_exists=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$GLOBAL_S3_BUCKET" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null)
|
||||
check "GLOBAL_S3_BUCKET ($GLOBAL_S3_BUCKET) exists" "true" "$storage_bucket_exists"
|
||||
else
|
||||
echo " SKIP: GLOBAL_S3_BUCKET not set"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. CreateBucket
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- CreateBucket ---"
|
||||
s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null 2>&1
|
||||
|
||||
# Verify create succeeded
|
||||
create_found=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null)
|
||||
check "CreateBucket" "true" "$create_found"
|
||||
|
||||
if [ "$create_found" != "true" ]; then
|
||||
echo " Cannot continue without a bucket. Aborting."
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify in ListBuckets (separate call)
|
||||
bucket_found=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null)
|
||||
check "Bucket visible in ListBuckets" "true" "$bucket_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. PutObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- PutObject ---"
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
echo "hello from backend test" > "$tmpfile"
|
||||
put_output=$(s3 s3 cp "$tmpfile" "s3://$bucket_name/test-file.txt")
|
||||
put_ok=$(echo "$put_output" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "PutObject" "true" "$put_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. ListObjectsV2
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- ListObjectsV2 ---"
|
||||
list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
object_found=$(echo "$list_objects" | \
|
||||
jq -r '[.Contents[]? | .Key] | if any(. == "test-file.txt") then "true" else "false" end' 2>/dev/null)
|
||||
check "Object found in ListObjectsV2" "true" "$object_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. HeadObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- HeadObject ---"
|
||||
head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "test-file.txt" --output json)
|
||||
head_size=$(echo "$head_output" | jq -r '.ContentLength // 0' 2>/dev/null)
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
check "HeadObject returns correct size" "$original_size" "$head_size"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. GetObject + content verify
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- GetObject ---"
|
||||
download_file=$(mktemp); cleanup_files="$cleanup_files $download_file"
|
||||
s3 s3 cp "s3://$bucket_name/test-file.txt" "$download_file" >/dev/null
|
||||
downloaded_content=$(cat "$download_file")
|
||||
check "GetObject content matches" "hello from backend test" "$downloaded_content"
|
||||
rm -f "$download_file"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. CopyObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- CopyObject ---"
|
||||
copy_output=$(s3 s3 cp "s3://$bucket_name/test-file.txt" "s3://$bucket_name/test-copy.txt")
|
||||
copy_ok=$(echo "$copy_output" | grep -q "copy:" && echo "true" || echo "false")
|
||||
check "CopyObject" "true" "$copy_ok"
|
||||
|
||||
copy_download=$(mktemp); cleanup_files="$cleanup_files $copy_download"
|
||||
s3 s3 cp "s3://$bucket_name/test-copy.txt" "$copy_download" >/dev/null
|
||||
check "Copied object content matches" "hello from backend test" "$(cat "$copy_download")"
|
||||
rm -f "$copy_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. DeleteObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- DeleteObject ---"
|
||||
s3 s3 rm "s3://$bucket_name/test-copy.txt" >/dev/null
|
||||
list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
copy_gone=$(echo "$list_after_delete" | \
|
||||
jq -r '[.Contents[]? | .Key] | if any(. == "test-copy.txt") then "false" else "true" end' 2>/dev/null)
|
||||
check "Deleted object no longer listed" "true" "$copy_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 10. Multipart upload (7MB)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Multipart upload (7MB) ---"
|
||||
large_file=$(mktemp); cleanup_files="$cleanup_files $large_file"
|
||||
dd if=/dev/urandom of="$large_file" bs=1048576 count=7 2>/dev/null
|
||||
large_size=$(wc -c < "$large_file" | tr -d ' ')
|
||||
large_put=$(s3 s3 cp "$large_file" "s3://$bucket_name/large-file.bin")
|
||||
large_ok=$(echo "$large_put" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "Multipart upload (7MB)" "true" "$large_ok"
|
||||
|
||||
large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json)
|
||||
remote_size=$(echo "$large_head" | jq -r '.ContentLength // 0' 2>/dev/null)
|
||||
check "Multipart size matches ($large_size bytes)" "$large_size" "$remote_size"
|
||||
|
||||
large_download=$(mktemp); cleanup_files="$cleanup_files $large_download"
|
||||
s3 s3 cp "s3://$bucket_name/large-file.bin" "$large_download" >/dev/null
|
||||
download_size=$(wc -c < "$large_download" | tr -d ' ')
|
||||
check "Multipart download size matches" "$large_size" "$download_size"
|
||||
rm -f "$large_file" "$large_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 11. DeleteObjects (batch delete)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- DeleteObjects (batch) ---"
|
||||
batch_file=$(mktemp); cleanup_files="$cleanup_files $batch_file"
|
||||
echo "batch-a" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-a.txt" >/dev/null
|
||||
echo "batch-b" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-b.txt" >/dev/null
|
||||
echo "batch-c" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-c.txt" >/dev/null
|
||||
rm -f "$batch_file"
|
||||
delete_objects_output=$(s3 s3api delete-objects --bucket "$bucket_name" \
|
||||
--delete '{"Objects":[{"Key":"batch-a.txt"},{"Key":"batch-b.txt"},{"Key":"batch-c.txt"}]}' \
|
||||
--output json)
|
||||
deleted_count=$(echo "$delete_objects_output" | jq -r '.Deleted | length' 2>/dev/null)
|
||||
check "DeleteObjects removed 3 objects" "3" "$deleted_count"
|
||||
|
||||
# Verify all gone
|
||||
batch_list=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --prefix "batch-" --output json)
|
||||
batch_remaining=$(echo "$batch_list" | jq -r '[.Contents[]?] | length' 2>/dev/null)
|
||||
check "Batch-deleted objects gone" "0" "$batch_remaining"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 12. Presigned URLs
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Presigned URLs ---"
|
||||
presign_file=$(mktemp); cleanup_files="$cleanup_files $presign_file"
|
||||
echo "presigned content test" > "$presign_file"
|
||||
s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null
|
||||
rm -f "$presign_file"
|
||||
|
||||
presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt")
|
||||
presign_body=$(curl -s "$presigned_url")
|
||||
check "Presigned URL returns correct content" "presigned content test" "$presign_body"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 13. Conditional request (IfNoneMatch)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Conditional request (IfNoneMatch) ---"
|
||||
# --if-none-match requires aws cli v2.22+ ; skip if not supported
|
||||
if aws s3api put-object help 2>&1 | grep -q 'if-none-match'; then
|
||||
cond_file=$(mktemp); cleanup_files="$cleanup_files $cond_file"
|
||||
echo "conditional test" > "$cond_file"
|
||||
|
||||
# First put should succeed (key doesn't exist)
|
||||
first_put_err=$(s3 s3api put-object --bucket "$bucket_name" --key "cond-test.txt" \
|
||||
--body "$cond_file" --if-none-match '*' --output json 2>&1 || true)
|
||||
first_put_ok=$(echo "$first_put_err" | grep -qi "error\|denied\|PreconditionFailed" && echo "false" || echo "true")
|
||||
check "IfNoneMatch put (new key) succeeds" "true" "$first_put_ok"
|
||||
|
||||
# Second put should fail with PreconditionFailed (key exists)
|
||||
cond_err=$(s3 s3api put-object --bucket "$bucket_name" --key "cond-test.txt" \
|
||||
--body "$cond_file" --if-none-match '*' --output json 2>&1 || true)
|
||||
cond_rejected=$(echo "$cond_err" | grep -qi "PreconditionFailed" && echo "true" || echo "false")
|
||||
check "IfNoneMatch put (existing key) rejected" "true" "$cond_rejected"
|
||||
rm -f "$cond_file"
|
||||
else
|
||||
echo " SKIP: aws cli does not support --if-none-match (requires v2.22+)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 14. Range request (partial GetObject)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Range request ---"
|
||||
range_file=$(mktemp); cleanup_files="$cleanup_files $range_file"
|
||||
echo "hello range test content" > "$range_file"
|
||||
s3 s3 cp "$range_file" "s3://$bucket_name/range-test.txt" >/dev/null
|
||||
rm -f "$range_file"
|
||||
|
||||
range_download=$(mktemp); cleanup_files="$cleanup_files $range_download"
|
||||
s3 s3api get-object --bucket "$bucket_name" --key "range-test.txt" \
|
||||
--range "bytes=0-4" "$range_download" --output json >/dev/null
|
||||
range_content=$(cat "$range_download")
|
||||
check "Range request returns partial content" "hello" "$range_content"
|
||||
rm -f "$range_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 15. Authentication
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Authentication ---"
|
||||
bad_output=$(AWS_ACCESS_KEY_ID="invalid-key" \
|
||||
AWS_SECRET_ACCESS_KEY="invalid-secret" \
|
||||
aws s3api list-buckets \
|
||||
--endpoint-url "$BACKEND_URL" \
|
||||
--region "$REGION" \
|
||||
--output json 2>&1 || true)
|
||||
bad_ok=$(echo "$bad_output" | grep -qi "denied\|invalid\|error\|403\|401" && echo "true" || echo "false")
|
||||
check "Invalid credentials rejected" "true" "$bad_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 16. Cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Cleanup ---"
|
||||
s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null
|
||||
s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null
|
||||
bucket_gone=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end' 2>/dev/null)
|
||||
check "Test bucket deleted" "true" "$bucket_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
292
tests/test-s3.sh
Normal file
292
tests/test-s3.sh
Normal file
@@ -0,0 +1,292 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test S3 protocol endpoint for self-hosted Supabase Storage.
|
||||
#
|
||||
# Verifies that the S3-compatible endpoint at /storage/v1/s3 works with
|
||||
# standard S3 clients — the same way end users interact with it via
|
||||
# aws cli, rclone, or other S3-compatible tools.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-s3.sh # Uses http://localhost:8000
|
||||
# sh test-s3.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance with S3 enabled:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
# - .env file with S3_PROTOCOL_ACCESS_KEY_ID, S3_PROTOCOL_ACCESS_KEY_SECRET, REGION
|
||||
# - aws cli v2 (for S3 operations)
|
||||
# - jq (for JSON parsing)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
S3_ENDPOINT="$BASE_URL/storage/v1/s3"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in aws jq; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read keys from .env
|
||||
S3_ACCESS_KEY=$(grep '^S3_PROTOCOL_ACCESS_KEY_ID=' .env | cut -d= -f2-)
|
||||
S3_SECRET_KEY=$(grep '^S3_PROTOCOL_ACCESS_KEY_SECRET=' .env | cut -d= -f2-)
|
||||
REGION=$(grep '^REGION=' .env | cut -d= -f2-)
|
||||
|
||||
if [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ]; then
|
||||
echo "Error: S3_PROTOCOL_ACCESS_KEY_ID or S3_PROTOCOL_ACCESS_KEY_SECRET not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrapper for aws s3/s3api commands with correct endpoint and credentials
|
||||
s3() {
|
||||
AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \
|
||||
AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \
|
||||
aws "$@" --endpoint-url "$S3_ENDPOINT" --region "$REGION" 2>&1
|
||||
}
|
||||
|
||||
bucket_name="s3-test-$$"
|
||||
|
||||
echo ""
|
||||
echo "=== S3 protocol test against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. S3 ListBuckets
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- S3 ListBuckets ---"
|
||||
list_output=$(s3 s3api list-buckets --output json)
|
||||
list_ok=$(echo "$list_output" | jq -r 'if .Buckets then "true" else "false" end' 2>/dev/null)
|
||||
check "ListBuckets returns valid response" "true" "$list_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. S3 CreateBucket
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 CreateBucket ---"
|
||||
s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null 2>&1
|
||||
|
||||
# Verify create succeeded
|
||||
create_found=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null)
|
||||
check "CreateBucket" "true" "$create_found"
|
||||
|
||||
if [ "$create_found" != "true" ]; then
|
||||
echo " Cannot continue without a bucket. Aborting."
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify bucket appears in ListBuckets (separate call)
|
||||
s3_bucket_found=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null)
|
||||
check "Bucket visible in ListBuckets" "true" "$s3_bucket_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. S3 PutObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 PutObject ---"
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
echo "hello from s3 upload test" > "$tmpfile"
|
||||
put_output=$(s3 s3 cp "$tmpfile" "s3://$bucket_name/s3-uploaded.txt")
|
||||
put_ok=$(echo "$put_output" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "PutObject upload" "true" "$put_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. S3 ListObjectsV2
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 ListObjectsV2 ---"
|
||||
list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
object_found=$(echo "$list_objects" | \
|
||||
jq -r '[.Contents[]? | .Key] | if any(. == "s3-uploaded.txt") then "true" else "false" end' 2>/dev/null)
|
||||
check "Object found in ListObjectsV2" "true" "$object_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. S3 HeadObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 HeadObject ---"
|
||||
head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "s3-uploaded.txt" --output json)
|
||||
head_size=$(echo "$head_output" | jq -r '.ContentLength // 0' 2>/dev/null)
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
check "HeadObject returns correct size" "$original_size" "$head_size"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. S3 GetObject (download) + content verify
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 GetObject ---"
|
||||
download_file=$(mktemp); cleanup_files="$cleanup_files $download_file"
|
||||
s3 s3 cp "s3://$bucket_name/s3-uploaded.txt" "$download_file" >/dev/null
|
||||
downloaded_content=$(cat "$download_file")
|
||||
check "GetObject content matches" "hello from s3 upload test" "$downloaded_content"
|
||||
rm -f "$download_file"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. S3 CopyObject (server-side copy)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 CopyObject ---"
|
||||
copy_output=$(s3 s3 cp "s3://$bucket_name/s3-uploaded.txt" "s3://$bucket_name/s3-copied.txt")
|
||||
copy_ok=$(echo "$copy_output" | grep -q "copy:" && echo "true" || echo "false")
|
||||
check "CopyObject" "true" "$copy_ok"
|
||||
|
||||
# Verify copied content
|
||||
copy_download=$(mktemp); cleanup_files="$cleanup_files $copy_download"
|
||||
s3 s3 cp "s3://$bucket_name/s3-copied.txt" "$copy_download" >/dev/null
|
||||
check "Copied object content matches" "hello from s3 upload test" "$(cat "$copy_download")"
|
||||
rm -f "$copy_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. S3 DeleteObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 DeleteObject ---"
|
||||
s3 s3 rm "s3://$bucket_name/s3-copied.txt" >/dev/null
|
||||
# Verify object is gone
|
||||
list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
copied_gone=$(echo "$list_after_delete" | \
|
||||
jq -r '[.Contents[]? | .Key] | if any(. == "s3-copied.txt") then "false" else "true" end' 2>/dev/null)
|
||||
check "Deleted object no longer listed" "true" "$copied_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. Multipart upload (>5MB triggers multipart)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 multipart upload (7MB) ---"
|
||||
large_file=$(mktemp); cleanup_files="$cleanup_files $large_file"
|
||||
dd if=/dev/urandom of="$large_file" bs=1048576 count=7 2>/dev/null
|
||||
large_size=$(wc -c < "$large_file" | tr -d ' ')
|
||||
large_put=$(s3 s3 cp "$large_file" "s3://$bucket_name/large-file.bin")
|
||||
large_ok=$(echo "$large_put" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "Multipart upload (7MB)" "true" "$large_ok"
|
||||
|
||||
# Verify size via HeadObject
|
||||
large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json)
|
||||
remote_size=$(echo "$large_head" | jq -r '.ContentLength // 0' 2>/dev/null)
|
||||
check "Multipart upload size matches ($large_size bytes)" "$large_size" "$remote_size"
|
||||
|
||||
# Download and verify size
|
||||
large_download=$(mktemp); cleanup_files="$cleanup_files $large_download"
|
||||
s3 s3 cp "s3://$bucket_name/large-file.bin" "$large_download" >/dev/null
|
||||
download_size=$(wc -c < "$large_download" | tr -d ' ')
|
||||
check "Multipart download size matches" "$large_size" "$download_size"
|
||||
rm -f "$large_file" "$large_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 10. Range request (partial GetObject)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Range request ---"
|
||||
range_file=$(mktemp); cleanup_files="$cleanup_files $range_file"
|
||||
echo "hello range test content" > "$range_file"
|
||||
s3 s3 cp "$range_file" "s3://$bucket_name/range-test.txt" >/dev/null
|
||||
rm -f "$range_file"
|
||||
|
||||
range_download=$(mktemp); cleanup_files="$cleanup_files $range_download"
|
||||
s3 s3api get-object --bucket "$bucket_name" --key "range-test.txt" \
|
||||
--range "bytes=0-4" "$range_download" --output json >/dev/null
|
||||
range_content=$(cat "$range_download")
|
||||
check "Range request returns partial content" "hello" "$range_content"
|
||||
rm -f "$range_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 11. Presigned URLs
|
||||
# ---------------------------------------------
|
||||
# Storage supports S3 presigned URLs (query-parameter auth), but Kong's
|
||||
# request-transformer adds an empty Authorization header when the Lua
|
||||
# expression evaluates to nil. Storage sees typeof "" === "string" and
|
||||
# enters parseAuthorizationHeader instead of parseQuerySignature.
|
||||
# This test will pass once the Kong config is fixed.
|
||||
|
||||
echo ""
|
||||
echo "--- Presigned URLs ---"
|
||||
presign_file=$(mktemp); cleanup_files="$cleanup_files $presign_file"
|
||||
echo "presigned content test" > "$presign_file"
|
||||
s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null
|
||||
rm -f "$presign_file"
|
||||
|
||||
presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt")
|
||||
presign_body=$(curl -s "$presigned_url")
|
||||
check "Presigned URL returns correct content" "presigned content test" "$presign_body"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 12. Authentication
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Authentication ---"
|
||||
bad_output=$(AWS_ACCESS_KEY_ID="invalid-key" \
|
||||
AWS_SECRET_ACCESS_KEY="invalid-secret" \
|
||||
aws s3api list-buckets \
|
||||
--endpoint-url "$S3_ENDPOINT" \
|
||||
--region "$REGION" \
|
||||
--output json 2>&1 || true)
|
||||
bad_ok=$(echo "$bad_output" | grep -qi "denied\|invalid\|error\|403\|401" && echo "true" || echo "false")
|
||||
check "Invalid credentials rejected" "true" "$bad_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 13. Cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Cleanup ---"
|
||||
s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null
|
||||
s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null
|
||||
# Verify bucket is gone
|
||||
bucket_gone=$(s3 s3api list-buckets --output json | \
|
||||
jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end' 2>/dev/null)
|
||||
check "Bucket deleted via S3" "true" "$bucket_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
351
tests/test-self-hosted.sh
Normal file
351
tests/test-self-hosted.sh
Normal file
@@ -0,0 +1,351 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Smoke test for self-hosted Supabase - verifies core functionality end-to-end.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-self-hosted.sh # Uses http://localhost:8000
|
||||
# sh test-self-hosted.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance
|
||||
# - .env file with keys configured
|
||||
# - jq (for JSON parsing)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: jq not found. Install it: https://jqlang.github.io/jq/download/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read keys from .env
|
||||
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
|
||||
DASHBOARD_USERNAME=$(grep '^DASHBOARD_USERNAME=' .env | cut -d= -f2-)
|
||||
DASHBOARD_PASSWORD=$(grep '^DASHBOARD_PASSWORD=' .env | cut -d= -f2-)
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
http_status() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
|
||||
}
|
||||
|
||||
http_body() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s "$@" "$url"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Self-hosted smoke test against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. Container health (via docker compose)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- Container health ---"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
container_status=$(docker compose ps --format json 2>/dev/null | jq -rs '
|
||||
[.[] | select(.State != "running" or (.Health != "" and .Health != "healthy"))]
|
||||
| (length | tostring) + "|" + ([.[] | .Service + ": State=" + .State + " Health=" + (.Health // "none")] | join(", "))
|
||||
' 2>/dev/null || echo "?|")
|
||||
unhealthy="${container_status%%|*}"
|
||||
container_issues="${container_status#*|}"
|
||||
if [ "$unhealthy" = "0" ]; then
|
||||
check "All containers healthy" "0" "$unhealthy"
|
||||
elif [ "$unhealthy" = "?" ]; then
|
||||
echo " SKIP: Could not check container health"
|
||||
else
|
||||
check "All containers healthy ($container_issues)" "0" "$unhealthy"
|
||||
fi
|
||||
else
|
||||
echo " SKIP: docker not available"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. Studio dashboard
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Studio dashboard ---"
|
||||
# Studio may redirect (307/302) after auth - follow redirects
|
||||
check "Studio accessible with basic auth" "200" \
|
||||
"$(http_status "$BASE_URL/" -L -u "$DASHBOARD_USERNAME:$DASHBOARD_PASSWORD")"
|
||||
check "Studio rejects without auth" "401" \
|
||||
"$(http_status "$BASE_URL/")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. Auth: create user, sign in, get user, public signup, delete
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Auth: user lifecycle ---"
|
||||
|
||||
test_email="smoke-test-$$@example.com"
|
||||
test_password="smoke-test-password-123456"
|
||||
|
||||
# Create user via admin API (works regardless of email autoconfirm setting)
|
||||
create_resp=$(http_body "$BASE_URL/auth/v1/admin/users" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\",\"email_confirm\":true}")
|
||||
|
||||
user_id=$(echo "$create_resp" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$user_id" ]; then
|
||||
check "Create user (admin)" "true" "true"
|
||||
|
||||
# Sign in via public endpoint
|
||||
signin_resp=$(http_body "$BASE_URL/auth/v1/token?grant_type=password" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
access_token=$(echo "$signin_resp" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$access_token" ]; then
|
||||
check "Sign in user" "true" "true"
|
||||
|
||||
# Get user profile with session JWT
|
||||
check "Get user profile" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/user" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
else
|
||||
check "Sign in user" "true" "false"
|
||||
fi
|
||||
|
||||
# Delete user
|
||||
delete_status=$(http_status "$BASE_URL/auth/v1/admin/users/$user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete user (admin)" "200" "$delete_status"
|
||||
else
|
||||
check "Create user (admin)" "true" "false"
|
||||
fi
|
||||
|
||||
# Public signup (optional — depends on email autoconfirm setting)
|
||||
signup_email="smoke-signup-$$@example.com"
|
||||
signup_resp=$(http_body "$BASE_URL/auth/v1/signup" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$signup_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
signup_token=$(echo "$signup_resp" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
signup_user_id=$(echo "$signup_resp" | jq -r '.id // .user.id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$signup_token" ]; then
|
||||
check "Public signup (autoconfirm on)" "true" "true"
|
||||
else
|
||||
echo " SKIP: Public signup (autoconfirm is off)"
|
||||
fi
|
||||
|
||||
# Clean up signup user if created
|
||||
if [ -n "$signup_user_id" ]; then
|
||||
http_status "$BASE_URL/auth/v1/admin/users/$signup_user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. PostgREST: query
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- PostgREST ---"
|
||||
check "REST API query" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. GraphQL
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- GraphQL ---"
|
||||
gql_resp=$(http_body "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')
|
||||
gql_has_data=$(echo "$gql_resp" | jq -r 'if .data then "true" else "false" end' 2>/dev/null)
|
||||
check "GraphQL introspection" "true" "$gql_has_data"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. Storage: create bucket, upload >6MB file, download, cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Storage: bucket + file lifecycle ---"
|
||||
|
||||
bucket_name="smoke-test-$$"
|
||||
|
||||
# Create bucket
|
||||
create_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"id\":\"$bucket_name\",\"name\":\"$bucket_name\",\"public\":true}")
|
||||
check "Create bucket" "200" "$create_bucket_status"
|
||||
|
||||
if [ "$create_bucket_status" = "200" ]; then
|
||||
# Generate a ~7MB file
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
dd if=/dev/urandom of="$tmpfile" bs=1048576 count=7 2>/dev/null
|
||||
|
||||
# Upload file
|
||||
upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$tmpfile")
|
||||
check "Upload 7MB file" "200" "$upload_status"
|
||||
|
||||
# Download file and verify size
|
||||
download_size=$(curl -s \
|
||||
"$BASE_URL/storage/v1/object/public/$bucket_name/test-large-file.bin" | wc -c | tr -d ' ')
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
check "Download file (size matches)" "true" \
|
||||
"$([ "$download_size" = "$original_size" ] && echo true || echo false)"
|
||||
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# Signed URL: upload a small file, create signed URL, fetch without auth
|
||||
sign_upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: text/plain" \
|
||||
--data-binary "signed url test content")
|
||||
check "Upload file for signing" "200" "$sign_upload_status"
|
||||
|
||||
if [ "$sign_upload_status" = "200" ]; then
|
||||
sign_resp=$(http_body "$BASE_URL/storage/v1/object/sign/$bucket_name/sign-test.txt" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"expiresIn": 600}')
|
||||
signed_path=$(echo "$sign_resp" | jq -r '.signedURL // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$signed_path" ]; then
|
||||
check "Create signed URL" "true" "true"
|
||||
# Fetch signed URL without any auth headers (goes through Kong)
|
||||
signed_content=$(curl -s "$BASE_URL/storage/v1$signed_path")
|
||||
check "Fetch signed URL (no auth)" "signed url test content" "$signed_content"
|
||||
else
|
||||
check "Create signed URL" "true" "false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Delete file
|
||||
delete_file_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete file" "200" "$delete_file_status"
|
||||
|
||||
# Delete signed test file
|
||||
http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
||||
|
||||
# Delete bucket
|
||||
delete_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket/$bucket_name" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete bucket" "200" "$delete_bucket_status"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. Edge Functions
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Edge Functions ---"
|
||||
fn_resp=$(http_body "$BASE_URL/functions/v1/hello" \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}')
|
||||
check "Call hello function" '"Hello from Edge Functions!"' "$fn_resp"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. pg-meta (Studio backend)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- pg-meta ---"
|
||||
check "pg-meta with service_role key" "200" \
|
||||
"$(http_status "$BASE_URL/pg/schemas" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY")"
|
||||
check "pg-meta rejects anon key" "403" \
|
||||
"$(http_status "$BASE_URL/pg/schemas" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
check "pg-meta rejects no key" "401" \
|
||||
"$(http_status "$BASE_URL/pg/schemas")"
|
||||
|
||||
echo ""
|
||||
echo "--- MCP (blocked by default) ---"
|
||||
check "/api/mcp blocked" "403" \
|
||||
"$(http_status "$BASE_URL/api/mcp")"
|
||||
check "/mcp blocked" "403" \
|
||||
"$(http_status "$BASE_URL/mcp")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. Realtime
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime ---"
|
||||
check "Realtime health" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" \
|
||||
-H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
223
utils/add-new-auth-keys.sh
Normal file
223
utils/add-new-auth-keys.sh
Normal file
@@ -0,0 +1,223 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Add asymmetric key pair and opaque API keys to a self-hosted Supabase installation.
|
||||
#
|
||||
# Reads JWT_SECRET from .env and generates:
|
||||
# - EC P-256 key pair (JWT_KEYS, JWT_JWKS)
|
||||
# - Opaque API keys (SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY)
|
||||
# - Internal: ES256 JWT API keys (ANON_KEY_ASYMMETRIC, SERVICE_ROLE_KEY_ASYMMETRIC)
|
||||
#
|
||||
# Usage:
|
||||
# sh add-new-auth-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh add-new-auth-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh add-new-auth-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .env file with JWT_SECRET set (run generate-keys.sh first)
|
||||
# - node (>= 16) or docker
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
node_ok() {
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||
[ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null
|
||||
}
|
||||
|
||||
# Resolve how to run node: local install (>= 16) preferred, docker fallback.
|
||||
if node_ok; then
|
||||
node_runner="node"
|
||||
else
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "Local node $(node -v) is too old (need >= 16), falling back to docker."
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: requires either node (>= 16) or docker."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Error: docker is installed but the daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect node:22-alpine >/dev/null 2>&1; then
|
||||
echo "Pulling node:22-alpine (first-run only)..."
|
||||
docker pull node:22-alpine
|
||||
fi
|
||||
|
||||
node_runner="docker run --rm node:22-alpine node"
|
||||
fi
|
||||
|
||||
# Read JWT_SECRET from .env
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jwt_secret=$(grep '^JWT_SECRET=' .env | cut -d= -f2- | tr -d '\r')
|
||||
if [ -z "$jwt_secret" ]; then
|
||||
echo "Error: JWT_SECRET not found in .env. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# Node.js does the crypto-heavy work:
|
||||
# - EC P-256 keypair generation
|
||||
# - JWKS construction (with symmetric key included)
|
||||
# - ES256 JWT signing
|
||||
# - Opaque API key generation with checksum
|
||||
$node_runner -e '
|
||||
const crypto = require("crypto");
|
||||
|
||||
const jwtSecret = process.argv[1];
|
||||
|
||||
// Generate EC P-256 keypair and export as JWK
|
||||
const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
||||
const jwkPrivate = privateKey.export({ format: "jwk" });
|
||||
|
||||
const kid = crypto.randomUUID();
|
||||
|
||||
// Symmetric key as JWK (base64url-encoded)
|
||||
const octKey = {
|
||||
kty: "oct",
|
||||
k: Buffer.from(jwtSecret).toString("base64url"),
|
||||
alg: "HS256"
|
||||
};
|
||||
|
||||
// JWKS with private key (for Auth to sign tokens)
|
||||
const jwksKeypair = { keys: [
|
||||
{ kty: "EC", kid, use: "sig", key_ops: ["sign", "verify"], alg: "ES256", ext: true,
|
||||
crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y, d: jwkPrivate.d },
|
||||
octKey
|
||||
]};
|
||||
|
||||
// JWKS with public key only (for PostgREST, Realtime, Storage to verify)
|
||||
const jwksPublic = { keys: [
|
||||
{ kty: "EC", kid, use: "sig", key_ops: ["verify"], alg: "ES256", ext: true,
|
||||
crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y },
|
||||
octKey
|
||||
]};
|
||||
|
||||
// Sign ES256 JWT
|
||||
function signES256(payload) {
|
||||
const header = { alg: "ES256", typ: "JWT", kid };
|
||||
const b64Header = Buffer.from(JSON.stringify(header)).toString("base64url");
|
||||
const b64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
const data = b64Header + "." + b64Payload;
|
||||
const sig = crypto.sign("SHA256", Buffer.from(data), {
|
||||
key: privateKey,
|
||||
dsaEncoding: "ieee-p1363"
|
||||
}).toString("base64url");
|
||||
return data + "." + sig;
|
||||
}
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const exp = iat + 5 * 365 * 24 * 3600; // 5 years
|
||||
|
||||
const anonJwt = signES256({ role: "anon", iss: "supabase", iat, exp });
|
||||
const serviceJwt = signES256({ role: "service_role", iss: "supabase", iat, exp });
|
||||
|
||||
// Generate opaque API keys with checksum
|
||||
const PROJECT_REF = "supabase-self-hosted";
|
||||
|
||||
function generateOpaqueKey(prefix) {
|
||||
const random = crypto.randomBytes(17).toString("base64url").slice(0, 22);
|
||||
const intermediate = prefix + random;
|
||||
const checksum = crypto.createHash("sha256")
|
||||
.update(PROJECT_REF + "|" + intermediate)
|
||||
.digest("base64url")
|
||||
.slice(0, 8);
|
||||
return intermediate + "_" + checksum;
|
||||
}
|
||||
|
||||
const publishableKey = generateOpaqueKey("sb_publishable_");
|
||||
const secretKey = generateOpaqueKey("sb_secret_");
|
||||
|
||||
// Output as KEY=value lines for shell to parse
|
||||
console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey);
|
||||
console.log("SUPABASE_SECRET_KEY=" + secretKey);
|
||||
console.log("ANON_KEY_ASYMMETRIC=" + anonJwt);
|
||||
console.log("SERVICE_ROLE_KEY_ASYMMETRIC=" + serviceJwt);
|
||||
console.log("JWT_KEYS=" + JSON.stringify(jwksKeypair.keys));
|
||||
console.log("JWT_JWKS=" + JSON.stringify(jwksPublic));
|
||||
' "$jwt_secret" > "$tmpdir/output"
|
||||
|
||||
# Read generated values
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
ANON_KEY_ASYMMETRIC=$(grep '^ANON_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=$(grep '^SERVICE_ROLE_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-)
|
||||
JWT_KEYS=$(grep '^JWT_KEYS=' "$tmpdir/output" | cut -d= -f2-)
|
||||
JWT_JWKS=$(grep '^JWT_JWKS=' "$tmpdir/output" | cut -d= -f2-)
|
||||
|
||||
echo ""
|
||||
echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}"
|
||||
echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}"
|
||||
echo ""
|
||||
echo "JWT_KEYS=${JWT_KEYS}"
|
||||
echo ""
|
||||
echo "JWT_JWKS=${JWT_JWKS}"
|
||||
echo ""
|
||||
echo "Ensure the following configuration is uncommented in docker-compose.yml for the asymmetric key pair to work:"
|
||||
echo ""
|
||||
echo " Auth: GOTRUE_JWT_KEYS: \${JWT_KEYS:-[]}"
|
||||
echo " Realtime: API_JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
|
||||
echo " Storage: JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
# Append new variables if they don't exist, or update them if they do
|
||||
for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY ANON_KEY_ASYMMETRIC SERVICE_ROLE_KEY_ASYMMETRIC JWT_KEYS JWT_JWKS; do
|
||||
eval "val=\$$var"
|
||||
if grep -q "^${var}=" .env; then
|
||||
sed -i.old -e "s|^${var}=.*$|${var}=${val}|" .env
|
||||
else
|
||||
echo "${var}=${val}" >> .env
|
||||
fi
|
||||
done
|
||||
|
||||
# Uncomment new auth configuration in docker-compose.yml
|
||||
echo "Updating docker-compose.yml..."
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "Error: docker-compose.yml not found in $(pwd)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always fall through to the grep check
|
||||
sed -i.old \
|
||||
-e '/^[ ]*#GOTRUE_JWT_KEYS:/ s/#//' \
|
||||
-e '/^[ ]*#API_JWT_JWKS:/ s/#//' \
|
||||
-e '/^[ ]*#JWT_JWKS:/ s/#//' \
|
||||
docker-compose.yml || true
|
||||
|
||||
if grep -q '^[ ]*GOTRUE_JWT_KEYS:' docker-compose.yml && \
|
||||
grep -q '^[ ]*API_JWT_JWKS:' docker-compose.yml && \
|
||||
grep -q '^[ ]*JWT_JWKS:' docker-compose.yml; then
|
||||
echo "Done."
|
||||
else
|
||||
echo "Warning: could not edit docker-compose.yml. Uncomment auth configuration manually."
|
||||
fi
|
||||
157
utils/db-passwd.sh
Normal file
157
utils/db-passwd.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Portions of this code are derived from Inder Singh's update-db-pass.sh
|
||||
# Copyright 2025 Inder Singh. Licensed under Apache License 2.0.
|
||||
# Original source:
|
||||
# https://github.com/singh-inder/supabase-automated-self-host/blob/main/docker/update-db-pass.sh
|
||||
#
|
||||
# GitHub discussion here:
|
||||
# https://github.com/supabase/supabase/issues/22605#issuecomment-3323382144
|
||||
#
|
||||
# Changed:
|
||||
# - POSIX shell compatibility
|
||||
# - No hardcoded values for database service and admin user
|
||||
# - Use .env for the admin user and database service port
|
||||
# - Does _not_ set password for supabase_read_only_user (this role is not
|
||||
# supposed to have a password)
|
||||
# - Print all values and confirm before updating
|
||||
# - Stop on any errors
|
||||
#
|
||||
# Heads up:
|
||||
# - Updating _analytics.source_backends is not needed after PR logflare#2069
|
||||
# - Newer Logflare versions use a different table and update connection string
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! docker compose version > /dev/null 2>&1; then
|
||||
echo "Docker Compose not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing .env file. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate random hex-only password to avoid issues with SQL/shell
|
||||
new_passwd="$(openssl rand -hex 16)"
|
||||
# If replacing with a custom password, avoid using @/?#:&
|
||||
# https://supabase.com/docs/guides/database/postgres/roles#passwords
|
||||
# new_passwd="d0notUseSpecialSymbolsForPq123-"
|
||||
|
||||
# Check Postgres service
|
||||
db_image_prefix="supabase.postgres:"
|
||||
|
||||
compose_output=$(docker compose ps \
|
||||
--format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null | \
|
||||
grep -m1 "^$db_image_prefix" || true)
|
||||
|
||||
if [ -z "$compose_output" ]; then
|
||||
echo "Postgres container not found. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_image=$(echo "$compose_output" | cut -f1)
|
||||
db_srv_name=$(echo "$compose_output" | cut -f2)
|
||||
db_srv_status=$(echo "$compose_output" | cut -f3)
|
||||
|
||||
case "$db_srv_status" in
|
||||
Up*)
|
||||
;;
|
||||
*)
|
||||
echo "Postgres container status: $db_srv_status"
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
db_srv_port=$(grep "^POSTGRES_PORT=" .env | cut -d '=' -f 2)
|
||||
port_source=" (.env):"
|
||||
if [ -z "$db_srv_port" ]; then
|
||||
db_srv_port="5432"
|
||||
port_source=" (default):"
|
||||
fi
|
||||
|
||||
db_admin_user="supabase_admin"
|
||||
|
||||
echo ""
|
||||
echo "*** Check configuration below before updating database passwords! ***"
|
||||
echo ""
|
||||
echo "Service name: $db_srv_name"
|
||||
echo "Service status: $db_srv_status"
|
||||
echo "Service port${port_source} $db_srv_port"
|
||||
echo "Image: $db_image"
|
||||
echo ""
|
||||
echo "Admin user: $db_admin_user"
|
||||
|
||||
if ! test -t 0; then
|
||||
echo ""
|
||||
echo "Running non-interactively. Not updating passwords."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "New database password: $new_passwd"
|
||||
echo ""
|
||||
|
||||
printf "Update database passwords? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Canceled. Not updating passwords."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Updating passwords..."
|
||||
echo "Connecting to the database service container..."
|
||||
|
||||
docker compose exec -T "$db_srv_name" psql -U "$db_admin_user" -d "_supabase" -v ON_ERROR_STOP=1 <<EOF
|
||||
alter user anon with password '${new_passwd}';
|
||||
alter user authenticated with password '${new_passwd}';
|
||||
alter user authenticator with password '${new_passwd}';
|
||||
alter user dashboard_user with password '${new_passwd}';
|
||||
alter user pgbouncer with password '${new_passwd}';
|
||||
alter user postgres with password '${new_passwd}';
|
||||
alter user service_role with password '${new_passwd}';
|
||||
alter user supabase_admin with password '${new_passwd}';
|
||||
alter user supabase_auth_admin with password '${new_passwd}';
|
||||
alter user supabase_functions_admin with password '${new_passwd}';
|
||||
alter user supabase_replication_admin with password '${new_passwd}';
|
||||
alter user supabase_storage_admin with password '${new_passwd}';
|
||||
|
||||
DROP SCHEMA _supavisor CASCADE;
|
||||
create schema if not exists _supavisor;
|
||||
alter schema _supavisor owner to supabase_admin;
|
||||
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = '_analytics'
|
||||
AND table_name = 'source_backends'
|
||||
) THEN
|
||||
UPDATE _analytics.source_backends
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{url}',
|
||||
'"postgresql://${db_admin_user}:${new_passwd}@${db_srv_name}:${db_srv_port}/postgres"',
|
||||
false
|
||||
)
|
||||
WHERE type = 'postgres';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOF
|
||||
|
||||
echo "Updating POSTGRES_PASSWORD in .env..."
|
||||
sed -i.old "s|^POSTGRES_PASSWORD=.*$|POSTGRES_PASSWORD=$new_passwd|" .env
|
||||
|
||||
echo ""
|
||||
echo "Success. To update and restart containers use:"
|
||||
echo ""
|
||||
echo "docker compose up -d --force-recreate"
|
||||
echo ""
|
||||
135
utils/generate-keys.sh
Normal file
135
utils/generate-keys.sh
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Generate secrets and legacy symmetric JWT API keys for self-hosted Supabase.
|
||||
#
|
||||
# Generates: JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, and other secrets
|
||||
# needed for a fresh installation.
|
||||
#
|
||||
# Usage:
|
||||
# sh generate-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh generate-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh generate-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Portions of this code are derived from Inder Singh's setup.sh shell script.
|
||||
# Copyright 2025 Inder Singh. Licensed under Apache License 2.0.
|
||||
# Original source: https://github.com/singh-inder/supabase-automated-self-host/blob/main/setup.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
gen_hex() {
|
||||
openssl rand -hex "$1"
|
||||
}
|
||||
|
||||
gen_base64() {
|
||||
openssl rand -base64 "$1"
|
||||
}
|
||||
|
||||
base64_url_encode() {
|
||||
openssl enc -base64 -A | tr '+/' '-_' | tr -d '='
|
||||
}
|
||||
|
||||
gen_token() {
|
||||
payload=$1
|
||||
payload_base64=$(printf %s "$payload" | base64_url_encode)
|
||||
header_base64=$(printf %s "$header" | base64_url_encode)
|
||||
signed_content="${header_base64}.${payload_base64}"
|
||||
signature=$(printf %s "$signed_content" | openssl dgst -binary -sha256 -hmac "$jwt_secret" | base64_url_encode)
|
||||
printf '%s' "${signed_content}.${signature}"
|
||||
}
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "Error: openssl is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jwt_secret="$(gen_base64 30)"
|
||||
|
||||
# Used in gen_token()
|
||||
header='{"alg":"HS256","typ":"JWT"}'
|
||||
iat=$(date +%s)
|
||||
exp=$((iat + 5 * 3600 * 24 * 365)) # 5 years
|
||||
|
||||
# Normalizes JSON formatting so that the token matches https://www.jwt.io/ results
|
||||
anon_payload="{\"role\":\"anon\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}"
|
||||
service_role_payload="{\"role\":\"service_role\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}"
|
||||
|
||||
#echo "anon_payload=$anon_payload"
|
||||
#echo "service_role_payload=$service_role_payload"
|
||||
|
||||
anon_key=$(gen_token "$anon_payload")
|
||||
service_role_key=$(gen_token "$service_role_payload")
|
||||
|
||||
secret_key_base=$(gen_base64 48)
|
||||
vault_enc_key=$(gen_hex 16)
|
||||
pg_meta_crypto_key=$(gen_base64 24)
|
||||
|
||||
logflare_public_access_token=$(gen_base64 24)
|
||||
logflare_private_access_token=$(gen_base64 24)
|
||||
|
||||
s3_protocol_access_key_id=$(gen_hex 16)
|
||||
s3_protocol_access_key_secret=$(gen_hex 32)
|
||||
|
||||
minio_root_password=$(gen_hex 16)
|
||||
|
||||
echo ""
|
||||
echo "JWT_SECRET=${jwt_secret}"
|
||||
echo ""
|
||||
#echo "Issued at: $iat"
|
||||
#echo "Expire: $exp"
|
||||
echo "ANON_KEY=${anon_key}"
|
||||
echo "SERVICE_ROLE_KEY=${service_role_key}"
|
||||
echo ""
|
||||
echo "SECRET_KEY_BASE=${secret_key_base}"
|
||||
echo "VAULT_ENC_KEY=${vault_enc_key}"
|
||||
echo "PG_META_CRYPTO_KEY=${pg_meta_crypto_key}"
|
||||
echo "LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}"
|
||||
echo "LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}"
|
||||
echo "S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}"
|
||||
echo "S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}"
|
||||
echo "MINIO_ROOT_PASSWORD=${minio_root_password}"
|
||||
echo ""
|
||||
|
||||
postgres_password=$(gen_hex 16)
|
||||
dashboard_password=$(gen_hex 16)
|
||||
|
||||
echo "POSTGRES_PASSWORD=${postgres_password}"
|
||||
echo "DASHBOARD_PASSWORD=${dashboard_password}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
sed \
|
||||
-i.old \
|
||||
-e "s|^JWT_SECRET=.*$|JWT_SECRET=${jwt_secret}|" \
|
||||
-e "s|^ANON_KEY=.*$|ANON_KEY=${anon_key}|" \
|
||||
-e "s|^SERVICE_ROLE_KEY=.*$|SERVICE_ROLE_KEY=${service_role_key}|" \
|
||||
-e "s|^SECRET_KEY_BASE=.*$|SECRET_KEY_BASE=${secret_key_base}|" \
|
||||
-e "s|^VAULT_ENC_KEY=.*$|VAULT_ENC_KEY=${vault_enc_key}|" \
|
||||
-e "s|^PG_META_CRYPTO_KEY=.*$|PG_META_CRYPTO_KEY=${pg_meta_crypto_key}|" \
|
||||
-e "s|^LOGFLARE_PUBLIC_ACCESS_TOKEN=.*$|LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}|" \
|
||||
-e "s|^LOGFLARE_PRIVATE_ACCESS_TOKEN=.*$|LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}|" \
|
||||
-e "s|^S3_PROTOCOL_ACCESS_KEY_ID=.*$|S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}|" \
|
||||
-e "s|^S3_PROTOCOL_ACCESS_KEY_SECRET=.*$|S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}|" \
|
||||
-e "s|^MINIO_ROOT_PASSWORD=.*$|MINIO_ROOT_PASSWORD=${minio_root_password}|" \
|
||||
-e "s|^POSTGRES_PASSWORD=.*$|POSTGRES_PASSWORD=${postgres_password}|" \
|
||||
-e "s|^DASHBOARD_PASSWORD=.*$|DASHBOARD_PASSWORD=${dashboard_password}|" \
|
||||
.env
|
||||
153
utils/reassign-owner.sh
Normal file
153
utils/reassign-owner.sh
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reassign ownership of public schema objects from supabase_admin to postgres.
|
||||
#
|
||||
# Context and documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/remove-superuser-access
|
||||
#
|
||||
# Credits:
|
||||
# Original version by Inder Singh.
|
||||
#
|
||||
# Usage:
|
||||
# sh utils/reassign-owner.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Postgres service
|
||||
db_image_prefix="supabase.postgres:"
|
||||
|
||||
compose_output=$(docker compose ps \
|
||||
--format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null |
|
||||
grep -m1 "^$db_image_prefix" || true)
|
||||
|
||||
if [ -z "$compose_output" ]; then
|
||||
echo "Postgres container not found. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_srv_name=$(echo "$compose_output" | cut -f2)
|
||||
db_srv_status=$(echo "$compose_output" | cut -f3)
|
||||
|
||||
case "$db_srv_status" in
|
||||
Up*)
|
||||
;;
|
||||
*)
|
||||
echo "Postgres container status: $db_srv_status"
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! test -t 0; then
|
||||
echo ""
|
||||
echo "Running non-interactively. Not reassigning ownership."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "Reassign public schema objects to postgres user? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Canceled. Not reassigning ownership."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
docker compose exec -T "$db_srv_name" psql -v ON_ERROR_STOP=1 -U supabase_admin -d postgres <<'EOF'
|
||||
\echo 'Current supabase_admin-owned objects in public schema:'
|
||||
SELECT c.relname, c.relkind, c.relowner::regrole
|
||||
FROM pg_class c
|
||||
WHERE c.relnamespace = 'public'::regnamespace
|
||||
AND c.relowner = 'supabase_admin'::regrole;
|
||||
|
||||
-- Reassign user objects in public schema from supabase_admin to postgres.
|
||||
-- (Only affects public schema; Supabase-managed schemas stay as-is.
|
||||
-- Extension-owned objects are skipped.)
|
||||
DO $$
|
||||
DECLARE
|
||||
rec record;
|
||||
rel_count int := 0;
|
||||
fn_count int := 0;
|
||||
type_count int := 0;
|
||||
BEGIN
|
||||
-- Tables, views, sequences, materialized views, partitioned tables
|
||||
FOR rec IN
|
||||
SELECT c.relname, c.relkind
|
||||
FROM pg_class c
|
||||
WHERE c.relnamespace = 'public'::regnamespace
|
||||
AND c.relowner = 'supabase_admin'::regrole
|
||||
AND c.relkind IN ('r', 'v', 'S', 'm', 'p')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_class'::regclass
|
||||
AND d.objid = c.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
ORDER BY CASE c.relkind
|
||||
WHEN 'p' THEN 0 -- partitioned parents first; cascades ownership to partitions
|
||||
WHEN 'm' THEN 1
|
||||
WHEN 'r' THEN 2
|
||||
WHEN 'v' THEN 3
|
||||
WHEN 'S' THEN 4
|
||||
END
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE public.%I OWNER TO postgres', rec.relname);
|
||||
rel_count := rel_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- Functions and procedures
|
||||
FOR rec IN
|
||||
SELECT p.oid, p.proname, pg_get_function_identity_arguments(p.oid) AS args
|
||||
FROM pg_proc p
|
||||
WHERE p.pronamespace = 'public'::regnamespace
|
||||
AND p.proowner = 'supabase_admin'::regrole
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_proc'::regclass
|
||||
AND d.objid = p.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
LOOP
|
||||
EXECUTE format('ALTER ROUTINE public.%I(%s) OWNER TO postgres', rec.proname, rec.args);
|
||||
fn_count := fn_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- Types (excluding array types and table-bound composites)
|
||||
FOR rec IN
|
||||
SELECT t.typname
|
||||
FROM pg_type t
|
||||
WHERE t.typnamespace = 'public'::regnamespace
|
||||
AND t.typowner = 'supabase_admin'::regrole
|
||||
AND t.typrelid = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_type el
|
||||
WHERE el.oid = t.typelem
|
||||
AND el.typarray = t.oid
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_type'::regclass
|
||||
AND d.objid = t.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
LOOP
|
||||
EXECUTE format('ALTER TYPE public.%I OWNER TO postgres', rec.typname);
|
||||
type_count := type_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'Reassigned % relation(s), % routine(s), % type(s) from supabase_admin to postgres.',
|
||||
rel_count, fn_count, type_count;
|
||||
END
|
||||
$$;
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
117
utils/rotate-new-api-keys.sh
Normal file
117
utils/rotate-new-api-keys.sh
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Rotate opaque API keys for a self-hosted Supabase installation.
|
||||
#
|
||||
# Regenerates SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY
|
||||
# without touching the asymmetric key pair (JWKS) or JWT tokens.
|
||||
#
|
||||
# Usage:
|
||||
# sh rotate-new-api-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh rotate-new-api-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh rotate-new-api-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .env file (run generate-keys.sh and add-new-auth-keys.sh first)
|
||||
# - node (>= 16) or docker
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
node_ok() {
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||
[ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null
|
||||
}
|
||||
|
||||
# Resolve how to run node: local install (>= 16) preferred, docker fallback.
|
||||
if node_ok; then
|
||||
node_runner="node"
|
||||
else
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "Local node $(node -v) is too old (need >= 16), falling back to docker."
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: requires either node (>= 16) or docker."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Error: docker is installed but the daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect node:22-alpine >/dev/null 2>&1; then
|
||||
echo "Pulling node:22-alpine (first-run only)..."
|
||||
docker pull node:22-alpine
|
||||
fi
|
||||
|
||||
node_runner="docker run --rm node:22-alpine node"
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
$node_runner -e '
|
||||
const crypto = require("crypto");
|
||||
|
||||
const PROJECT_REF = "supabase-self-hosted";
|
||||
|
||||
function generateOpaqueKey(prefix) {
|
||||
const random = crypto.randomBytes(17).toString("base64url").slice(0, 22);
|
||||
const intermediate = prefix + random;
|
||||
const checksum = crypto.createHash("sha256")
|
||||
.update(PROJECT_REF + "|" + intermediate)
|
||||
.digest("base64url")
|
||||
.slice(0, 8);
|
||||
return intermediate + "_" + checksum;
|
||||
}
|
||||
|
||||
const publishableKey = generateOpaqueKey("sb_publishable_");
|
||||
const secretKey = generateOpaqueKey("sb_secret_");
|
||||
|
||||
console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey);
|
||||
console.log("SUPABASE_SECRET_KEY=" + secretKey);
|
||||
' > "$tmpdir/output"
|
||||
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
|
||||
echo ""
|
||||
echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}"
|
||||
echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY; do
|
||||
eval "val=\$$var"
|
||||
if grep -q "^${var}=" .env; then
|
||||
sed -i.old -e "s|^${var}=.*$|${var}=${val}|" .env
|
||||
else
|
||||
echo "${var}=${val}" >> .env
|
||||
fi
|
||||
done
|
||||
754
utils/upgrade-pg17.sh
Normal file
754
utils/upgrade-pg17.sh
Normal file
@@ -0,0 +1,754 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Requires bash (not sh) for pipefail, which ensures failures in piped
|
||||
# commands are caught during the upgrade.
|
||||
#
|
||||
# Upgrade self-hosted Supabase Postgres from 15 to 17.
|
||||
#
|
||||
# Uses Supabase's pg_upgrade scripts (initiate.sh + complete.sh) inside a
|
||||
# temporary PG15 container, then swaps data directories and starts Postgres 17.
|
||||
#
|
||||
# Usage (must be run as root or with sudo):
|
||||
# cd docker/
|
||||
# sudo bash utils/upgrade-pg17.sh # Interactive (prompts for confirmation)
|
||||
# sudo bash utils/upgrade-pg17.sh --yes # Non-interactive (skip all prompts)
|
||||
#
|
||||
# Requirements:
|
||||
# - Docker with Docker Compose (docker compose, not docker-compose)
|
||||
# - Running Supabase self-hosted setup with Postgres 15
|
||||
# - At least 2x current database size + 5 GB free disk space
|
||||
#
|
||||
# Backup:
|
||||
# The original Postgres 15 data directory is preserved as
|
||||
# ./volumes/db/data.bak.pg15 during the upgrade.
|
||||
# DO NOT DELETE it until you have verified the upgrade was successful.
|
||||
#
|
||||
# Rollback (if the upgrade fails or you want to revert):
|
||||
# 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down
|
||||
# 2. rm -rf ./volumes/db/data
|
||||
# 3. mv ./volumes/db/data.bak.pg15 ./volumes/db/data
|
||||
# 4. docker compose run --rm db chown -R postgres:postgres /etc/postgresql-custom/
|
||||
# 5. docker compose up -d
|
||||
#
|
||||
|
||||
# Ensure we're running under bash (not sh/zsh/dash).
|
||||
# Check that $BASH ends with /bash (not /sh, /zsh, etc.).
|
||||
case "${BASH:-}" in
|
||||
*/bash) ;;
|
||||
*) echo "Error: This script requires bash. Run it with: sudo bash $0" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AUTO_CONFIRM=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--yes|-y) AUTO_CONFIRM=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Configuration ----------------------------------------------------------
|
||||
|
||||
# Image used for the upgrade tarball + complete.sh container.
|
||||
# Must share glibc with PG15 (the extracted ELF binaries run inside PG15).
|
||||
PG17_UPGRADE_IMAGE="supabase/postgres:17.6.1.063"
|
||||
# Tag in supabase/postgres repo matching the upgrade image (for downloading scripts)
|
||||
PG17_SCRIPTS_REF="17.6.1.063"
|
||||
DB_CONTAINER="supabase-db"
|
||||
UPGRADE_CONTAINER="supabase-pg-upgrade"
|
||||
COMPLETE_CONTAINER="supabase-pg-complete"
|
||||
|
||||
DATA_DIR="./volumes/db/data"
|
||||
BACKUP_DIR="./volumes/db/data.bak.pg15"
|
||||
# Include image tag in cache filename so changing PG17_UPGRADE_IMAGE invalidates it
|
||||
PG17_TAG="${PG17_UPGRADE_IMAGE##*:}"
|
||||
TARBALL_CACHE="./volumes/db/pg17_upgrade_bin_${PG17_TAG}.tar.gz"
|
||||
# initiate.sh writes pg_upgrade output here: pgdata/, conf/, sql/
|
||||
MIGRATION_DIR="./volumes/db/data_migration"
|
||||
|
||||
# --- Helpers ----------------------------------------------------------------
|
||||
|
||||
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||
info() { printf '\n==> %s\n' "$*"; }
|
||||
warn() { printf 'Warning: %s\n' "$*" >&2; }
|
||||
|
||||
# Temp dir on host for tarball + scripts (mounted into containers)
|
||||
staging_dir=""
|
||||
pg_password=""
|
||||
current_image=""
|
||||
drop_extensions=""
|
||||
db_config_vol=""
|
||||
|
||||
# Remove leftover containers and staging dir on exit.
|
||||
# Uses an alpine container for rm because the tarball build runs as root
|
||||
# inside Docker - the resulting files are root-owned and can't be deleted
|
||||
# by the host user on macOS.
|
||||
cleanup() {
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
if [ -n "$staging_dir" ] && [ -d "$staging_dir" ]; then
|
||||
docker run --rm -v "$staging_dir:/cleanup" alpine rm -rf /cleanup 2>/dev/null || true
|
||||
rm -rf "$staging_dir" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
on_interrupt() {
|
||||
echo ""
|
||||
warn "Interrupted. Cleaning up..."
|
||||
# If db-config was chowned to PG17, restore for PG15 rollback
|
||||
if [ -n "$db_config_vol" ] && [ -n "$current_image" ]; then
|
||||
docker run --rm -v "${db_config_vol}:/vol" "$current_image" \
|
||||
chown -R postgres:postgres /vol/ 2>/dev/null || true
|
||||
fi
|
||||
die "Interrupted."
|
||||
}
|
||||
trap on_interrupt INT
|
||||
|
||||
confirm() {
|
||||
if [ "$AUTO_CONFIRM" = true ]; then return 0; fi
|
||||
if ! test -t 0; then
|
||||
die "This script must be run interactively, or use --yes to skip prompts."
|
||||
fi
|
||||
printf '%s (y/N) ' "$1"
|
||||
read -r reply
|
||||
case "$reply" in
|
||||
[Yy]*) return 0 ;;
|
||||
*) echo "Aborted."; exit 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_sql_on() {
|
||||
local container=$1; shift
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$container" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 "$@"
|
||||
}
|
||||
|
||||
wait_for_healthy() {
|
||||
local container=$1 retries=30
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec "$container" pg_isready -U postgres -h localhost >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 1
|
||||
done
|
||||
die "Postgres in '$container' did not become ready in 30 seconds."
|
||||
}
|
||||
|
||||
# --- Pre-flight checks -----------------------------------------------------
|
||||
|
||||
preflight() {
|
||||
info "Running pre-flight checks"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "This script must be run as root (e.g. sudo bash $0)."
|
||||
fi
|
||||
|
||||
docker compose version >/dev/null 2>&1 || die "Docker Compose not found."
|
||||
command -v curl >/dev/null 2>&1 || die "curl is required (for downloading upgrade scripts)."
|
||||
[ -f docker-compose.yml ] || die "Run this script from the docker/ directory."
|
||||
[ -f docker-compose.pg17.yml ] || die "Missing docker-compose.pg17.yml."
|
||||
[ -f .env ] || die "Missing .env file."
|
||||
|
||||
# Resolve db-config volume (exact match on _db-config suffix or bare db-config)
|
||||
db_config_vol=$(docker volume ls --filter "name=db-config" --format '{{.Name}}' \
|
||||
| grep -E '^db-config$|_db-config$' | head -n 1)
|
||||
[ -n "$db_config_vol" ] || die "Could not find db-config volume. Is Supabase running?"
|
||||
|
||||
# Read the target PG17 image from the compose override (what the user will run)
|
||||
PG17_TARGET_IMAGE=$(grep 'image:.*postgres' docker-compose.pg17.yml | awk '{print $2}' | head -n 1)
|
||||
[ -n "$PG17_TARGET_IMAGE" ] || die "Could not read image from docker-compose.pg17.yml."
|
||||
|
||||
pg_password=$(grep '^POSTGRES_PASSWORD=' .env | cut -d '=' -f 2- | sed "s/^['\"]//;s/['\"]$//" | head -n 1)
|
||||
[ -n "$pg_password" ] || die "POSTGRES_PASSWORD not set in .env."
|
||||
|
||||
docker inspect "$DB_CONTAINER" >/dev/null 2>&1 \
|
||||
|| die "Container '$DB_CONTAINER' not found. Is Supabase running?"
|
||||
|
||||
current_image=$(docker inspect "$DB_CONTAINER" --format '{{.Config.Image}}')
|
||||
case "$current_image" in
|
||||
supabase/postgres:15.*|supabase.postgres:15.*) ;;
|
||||
supabase/postgres:17.*|supabase.postgres:17.*) die "Already running Postgres 17 ($current_image)." ;;
|
||||
*) die "Unexpected database image: $current_image" ;;
|
||||
esac
|
||||
|
||||
local status
|
||||
status=$(docker inspect "$DB_CONTAINER" --format '{{.State.Status}}')
|
||||
[ "$status" = "running" ] || die "'$DB_CONTAINER' is not running (status: $status)."
|
||||
[ -d "$DATA_DIR" ] || die "Data directory not found: $DATA_DIR"
|
||||
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
warn "Backup directory already exists: $BACKUP_DIR"
|
||||
warn "This is likely from a previous upgrade attempt."
|
||||
warn "If you haven't verified that previous upgrade, roll back first:"
|
||||
warn " 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down"
|
||||
warn " 2. rm -rf $DATA_DIR"
|
||||
warn " 3. mv $BACKUP_DIR $DATA_DIR"
|
||||
warn " 4. docker compose run --rm db chown -R postgres:postgres /etc/postgresql-custom/"
|
||||
warn " 5. docker compose up -d"
|
||||
echo ""
|
||||
warn "Continuing will DELETE the existing backup permanently."
|
||||
confirm "Delete $BACKUP_DIR and start a fresh upgrade?"
|
||||
rm -rf "$BACKUP_DIR"
|
||||
fi
|
||||
if [ -d "$MIGRATION_DIR" ]; then
|
||||
rm -rf "$MIGRATION_DIR"
|
||||
fi
|
||||
|
||||
# Disk space
|
||||
local data_size_kb data_size_mb avail_kb avail_mb needed_mb
|
||||
data_size_kb=$(du -sk "$DATA_DIR" 2>/dev/null | cut -f1)
|
||||
[ -n "$data_size_kb" ] || die "Could not calculate data size for $DATA_DIR"
|
||||
data_size_mb=$((data_size_kb / 1024))
|
||||
avail_kb=$(df -k "$(dirname "$DATA_DIR")" | awk 'NR==2 { print $4 }')
|
||||
[ -n "$avail_kb" ] || die "Could not calculate available disk space for $(dirname "$DATA_DIR")"
|
||||
avail_mb=$((avail_kb / 1024))
|
||||
needed_mb=$((data_size_mb * 2 + 5000))
|
||||
echo " Data size: ${data_size_mb} MB"
|
||||
echo " Available space: ${avail_mb} MB"
|
||||
echo " Estimated need: ${needed_mb} MB"
|
||||
if [ "$avail_mb" -lt "$needed_mb" ]; then
|
||||
warn "Disk space may be insufficient."
|
||||
warn "pg_upgrade copies data; need ~2x data size + ~5 GB for the upgrade tarball."
|
||||
confirm "Continue anyway?"
|
||||
fi
|
||||
|
||||
# Incompatible extensions
|
||||
info "Checking for incompatible extensions"
|
||||
local incompatible
|
||||
incompatible=$(run_sql_on "$DB_CONTAINER" -A -t -c "
|
||||
SELECT string_agg(extname, ', ')
|
||||
FROM pg_extension
|
||||
WHERE extname IN ('timescaledb', 'plv8', 'plcoffee', 'plls');
|
||||
" 2>/dev/null | tr -d '[:space:]') || true
|
||||
|
||||
if [ -n "$incompatible" ]; then
|
||||
warn "Incompatible extensions found: $incompatible"
|
||||
warn "These do not exist in Postgres 17 and must be dropped before upgrading."
|
||||
warn "If you proceed, they will be dropped automatically."
|
||||
warn "The original data is preserved as a backup so you can roll back."
|
||||
confirm "Drop these extensions and continue with the upgrade?"
|
||||
drop_extensions="$incompatible"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " 1. Pull the Postgres 17 image"
|
||||
echo " 2. Build an upgrade tarball from the image (~1.2 GB compressed, temporary)"
|
||||
echo " 3. Stop all Supabase services"
|
||||
echo " 4. Run pg_upgrade (Postgres 15 -> 17)"
|
||||
echo " 5. Apply post-upgrade patches"
|
||||
echo " 6. Start Supabase with Postgres 17"
|
||||
echo " 7. Apply additional migrations"
|
||||
echo ""
|
||||
echo " Current image: $current_image"
|
||||
echo " Target image: $PG17_TARGET_IMAGE"
|
||||
echo " Upgrade image: $PG17_UPGRADE_IMAGE"
|
||||
echo " Data directory: $DATA_DIR"
|
||||
echo " Backup location: $BACKUP_DIR"
|
||||
echo ""
|
||||
confirm "Proceed with the upgrade?"
|
||||
}
|
||||
|
||||
# --- Step 1: Pull Postgres 17 image ----------------------------------------
|
||||
|
||||
pull_image() {
|
||||
info "Pulling Postgres 17 images"
|
||||
docker pull "$PG17_UPGRADE_IMAGE"
|
||||
if [ "$PG17_TARGET_IMAGE" != "$PG17_UPGRADE_IMAGE" ]; then
|
||||
docker pull "$PG17_TARGET_IMAGE"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 2: Build upgrade tarball -----------------------------------------
|
||||
#
|
||||
# Extracts PG17 binaries, libraries, share data, and upgrade scripts from
|
||||
# the PG17 Docker image into a tarball that initiate.sh can consume.
|
||||
#
|
||||
# The tarball uses the "non-nix" layout (17/bin, 17/lib, 17/share - no
|
||||
# nix_flake_version file), so initiate.sh sets LD_LIBRARY_PATH to find
|
||||
# the bundled libraries.
|
||||
|
||||
build_tarball() {
|
||||
local tmpbase="${TMPDIR:-/tmp}"
|
||||
staging_dir=$(mktemp -d "${tmpbase%/}/supabase-pg17-upgrade.XXXXXX")
|
||||
# World-writable so Docker containers can write to bind mounts on macOS,
|
||||
# where the VM's root user has no special access to host directories.
|
||||
chmod 777 "$staging_dir"
|
||||
echo " Staging directory: $staging_dir"
|
||||
|
||||
# Download upgrade scripts from the supabase/postgres repo (pinned to PG17_SCRIPTS_REF).
|
||||
# These are no longer bundled in the latest PG17 Docker images.
|
||||
info "Downloading upgrade scripts (ref: $PG17_SCRIPTS_REF)"
|
||||
local scripts_base="https://raw.githubusercontent.com/supabase/postgres/${PG17_SCRIPTS_REF}/ansible/files/admin_api_scripts/pg_upgrade_scripts"
|
||||
mkdir -p "$staging_dir/scripts"
|
||||
for script in initiate.sh complete.sh common.sh pgsodium_getkey.sh check.sh prepare.sh; do
|
||||
curl -fsSL "$scripts_base/$script" -o "$staging_dir/scripts/$script" \
|
||||
|| die "Failed to download $script from GitHub"
|
||||
done
|
||||
|
||||
if [ -f "$TARBALL_CACHE" ]; then
|
||||
info "Using cached upgrade tarball: $TARBALL_CACHE"
|
||||
cp "$TARBALL_CACHE" "$staging_dir/pg_upgrade_bin.tar.gz"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Building upgrade tarball from Postgres 17 image (first run)"
|
||||
docker run --rm --user root --entrypoint bash \
|
||||
-v "$staging_dir:/export" \
|
||||
"$PG17_UPGRADE_IMAGE" \
|
||||
-c '
|
||||
set -euo pipefail
|
||||
mkdir -p /export/17/bin /export/17/lib /export/17/share
|
||||
|
||||
echo " Copying binaries..."
|
||||
# Binaries in the nix profile are either ELF binaries or shell
|
||||
# wrappers that exec a .xxx-wrapped ELF from the nix store.
|
||||
# Extract the actual ELF binaries so they work outside nix.
|
||||
BIN_DIR=$(dirname $(readlink -f /usr/lib/postgresql/bin/postgres))
|
||||
for f in "$BIN_DIR"/*; do
|
||||
name=$(basename "$f")
|
||||
|
||||
# Skip nix wrapper-internal files
|
||||
case "$name" in .*-wrapped) continue ;; esac
|
||||
|
||||
# Check for ELF
|
||||
if [ -x "$f" ] && file -b "$f" | grep -q "ELF .* executable"; then
|
||||
cp "$f" /export/17/bin/"$name"
|
||||
else
|
||||
# Shell wrapper - extract the real .xxx-wrapped ELF path
|
||||
wrapped=$(grep -o "/nix/store/[^ \"]*-wrapped" "$f" 2>/dev/null | head -n 1 || true)
|
||||
if [ -n "$wrapped" ] && [ -f "$wrapped" ]; then
|
||||
cp "$wrapped" /export/17/bin/"$name"
|
||||
else
|
||||
cp "$f" /export/17/bin/"$name"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo " Copying libraries..."
|
||||
PKGLIBDIR=$(pg_config --pkglibdir)
|
||||
LIBDIR=$(pg_config --libdir)
|
||||
|
||||
# These paths may overlap (PKGLIBDIR and LIBDIR often point to the
|
||||
# same nix store path). Use cp -Lf to handle overwrites from
|
||||
# read-only nix store source files.
|
||||
cp -Lf "$PKGLIBDIR"/*.so /export/17/lib/ || echo " Warning: cp from $PKGLIBDIR failed" >&2
|
||||
cp -Lf "$LIBDIR"/*.so* /export/17/lib/ || echo " Warning: cp from $LIBDIR failed" >&2
|
||||
cp -Lf /nix/var/nix/profiles/default/lib/*.so* /export/17/lib/ || echo " Warning: cp from nix profile lib failed" >&2
|
||||
|
||||
echo " Copying share data..."
|
||||
|
||||
# Nix-built binaries resolve share dir relative to their location:
|
||||
# <bindir>/../share/postgresql/
|
||||
# so we need share/postgresql/ not just share/
|
||||
mkdir -p /export/17/share/postgresql
|
||||
|
||||
# Remove cyclic symlink (timezonesets/timezonesets -> timezonesets).
|
||||
rm -f /usr/share/postgresql/timezonesets/timezonesets 2>/dev/null || true
|
||||
|
||||
# Pre-create subdirectories so cp -rL does not need to mkdir them.
|
||||
# On macOS with Docker Desktop bind mount rejects mkdir with the nix store
|
||||
# read-only (dr-xr-xr-x) permissions; pre-creating with default
|
||||
# writable permissions fixed this
|
||||
mkdir -p /export/17/share/postgresql/{extension,timezonesets,tsearch_data}
|
||||
mkdir -p /export/17/share/postgresql/extension/{functions,procedures,tables,types}
|
||||
|
||||
cp -rL /usr/share/postgresql/* /export/17/share/postgresql/ || echo " Warning: cp share data had errors" >&2
|
||||
|
||||
# initiate.sh copies .control/.sql from PGLIBNEW to PGSHARENEW/extension/
|
||||
echo " Copying extension definitions to lib..."
|
||||
SHAREDIR=$(pg_config --sharedir)
|
||||
cp "$SHAREDIR"/extension/*.control /export/17/lib/ || echo " Warning: cp .control from $SHAREDIR/extension failed" >&2
|
||||
cp "$SHAREDIR"/extension/*.sql /export/17/lib/ || echo " Warning: cp .sql from $SHAREDIR/extension failed" >&2
|
||||
|
||||
# Verify critical files before creating tarball
|
||||
echo " Checking for key files..."
|
||||
[ -f /export/17/bin/postgres ] || { echo "Error: bin/postgres missing"; exit 1; }
|
||||
[ -f /export/17/share/postgresql/timezonesets/Default ] || { echo "Error: timezonesets/Default missing"; exit 1; }
|
||||
ls /export/17/share/postgresql/extension/*.control >/dev/null 2>&1 || { echo "Error: no .control files in extension/"; exit 1; }
|
||||
ls /export/17/lib/*.so >/dev/null 2>&1 || { echo "Error: no .so files in lib/"; exit 1; }
|
||||
|
||||
echo " Creating tarball (this may take several minutes)..."
|
||||
cd /export && tar czf pg_upgrade_bin.tar.gz 17/
|
||||
|
||||
echo " Tarball: $(du -sh /export/pg_upgrade_bin.tar.gz | cut -f1)"
|
||||
'
|
||||
|
||||
# Cache for next run
|
||||
cp "$staging_dir/pg_upgrade_bin.tar.gz" "$TARBALL_CACHE"
|
||||
info "Tarball cached at $TARBALL_CACHE"
|
||||
}
|
||||
|
||||
# --- Step 3: Drop incompatible extensions ----------------------------------
|
||||
|
||||
drop_incompatible_extensions() {
|
||||
if [ -z "$drop_extensions" ]; then
|
||||
return
|
||||
fi
|
||||
info "Dropping incompatible extensions"
|
||||
|
||||
local ext
|
||||
echo "$drop_extensions" | tr ',' '\n' | while read -r ext; do
|
||||
ext=$(echo "$ext" | tr -d '[:space:]')
|
||||
[ -z "$ext" ] && continue
|
||||
echo " DROP EXTENSION $ext CASCADE"
|
||||
run_sql_on "$DB_CONTAINER" -c "DROP EXTENSION IF EXISTS \"$ext\" CASCADE;"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Step 4: Stop services and back up -------------------------------------
|
||||
|
||||
stop_and_backup() {
|
||||
info "Backing up pgsodium root key"
|
||||
local key_backup="./volumes/db/pgsodium_root.key.bak.pg15"
|
||||
docker run --rm -v "${db_config_vol}:/src:ro" -v "$(pwd)/volumes/db:/dst" \
|
||||
alpine cp /src/pgsodium_root.key /dst/pgsodium_root.key.bak.pg15 \
|
||||
|| die "Failed to back up pgsodium root key from db-config volume."
|
||||
echo " Saved to: $key_backup"
|
||||
|
||||
info "Stopping all Supabase services"
|
||||
docker compose down
|
||||
|
||||
echo " Original data will be preserved as: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
# --- Step 5: Run pg_upgrade via initiate.sh + complete.sh ------------------
|
||||
#
|
||||
# Host directories are mounted at non-standard paths (/mnt/host-*) with
|
||||
# symlinks at the paths the upgrade scripts expect. This lets complete.sh's
|
||||
# CI wrapper (which does rm/mv/ln on /var/lib/postgresql/data and
|
||||
# /data_migration) operate on symlinks rather than bind mounts.
|
||||
|
||||
run_upgrade() {
|
||||
local abs_data_dir abs_migration_dir
|
||||
|
||||
mkdir -p "$MIGRATION_DIR"
|
||||
# World-writable for macOS Docker bind mount compatibility (see build_tarball)
|
||||
chmod 777 "$MIGRATION_DIR"
|
||||
abs_data_dir=$(cd "$DATA_DIR" && pwd)
|
||||
abs_migration_dir=$(cd "$MIGRATION_DIR" && pwd)
|
||||
|
||||
info "Starting upgrade container"
|
||||
docker run -d --name "$UPGRADE_CONTAINER" \
|
||||
--entrypoint sleep \
|
||||
-v "${abs_data_dir}:/mnt/host-pgdata" \
|
||||
-v "${abs_migration_dir}:/mnt/host-migration" \
|
||||
-v "${db_config_vol}:/etc/postgresql-custom" \
|
||||
-v "${staging_dir}:/tmp/staging:ro" \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$current_image" \
|
||||
infinity
|
||||
|
||||
info "Preparing upgrade environment"
|
||||
docker exec "$UPGRADE_CONTAINER" bash -c '
|
||||
# Symlink bind mounts to the paths the upgrade scripts expect
|
||||
rm -rf /var/lib/postgresql/data
|
||||
ln -s /mnt/host-pgdata /var/lib/postgresql/data
|
||||
ln -s /mnt/host-migration /data_migration
|
||||
|
||||
mkdir -p /tmp/persistent /tmp/upgrade /tmp/pg_upgrade
|
||||
cp /tmp/staging/pg_upgrade_bin.tar.gz /tmp/persistent/
|
||||
cp /tmp/staging/scripts/*.sh /tmp/upgrade/
|
||||
chmod +x /tmp/upgrade/*.sh
|
||||
|
||||
# Patch CI_start_postgres to use "restart" instead of "start" so it
|
||||
# is idempotent (initiate.sh starts postgres for top-level queries,
|
||||
# then handle_extensions calls CI_start_postgres again)
|
||||
sed -i "s/pg_ctl start -o/pg_ctl restart -o/g" /tmp/upgrade/common.sh
|
||||
|
||||
# Patch PGSHARENEW to match nix binary expectations (share/postgresql/)
|
||||
sed -i "s|PGSHARENEW=\"\$PG_UPGRADE_BIN_DIR/share\"|PGSHARENEW=\"\$PG_UPGRADE_BIN_DIR/share/postgresql\"|" /tmp/upgrade/initiate.sh
|
||||
'
|
||||
|
||||
info "Starting Postgres 15 in upgrade container"
|
||||
docker exec "$UPGRADE_CONTAINER" bash -c '
|
||||
su postgres -c "pg_ctl start -o \"-c config_file=/etc/postgresql/postgresql.conf\" -l /tmp/postgres.log"
|
||||
'
|
||||
wait_for_healthy "$UPGRADE_CONTAINER"
|
||||
|
||||
# initiate.sh expects the PG17 binaries tarball at /tmp/persistent/pg_upgrade_bin.tar.gz
|
||||
# (hardcoded path - copied there during container setup above).
|
||||
#
|
||||
# Env vars for the unwrapped nix ELF binaries in the tarball:
|
||||
# LD_LIBRARY_PATH - find libpq, libssl, etc. (RUNPATH points to absent nix store paths)
|
||||
# NIX_PGLIBDIR - postgres uses this to find extension .so files
|
||||
|
||||
info "Running initiate.sh (pg_upgrade: Postgres 15 -> 17)"
|
||||
echo " This may take several minutes depending on database size..."
|
||||
echo ""
|
||||
if ! docker exec \
|
||||
-e IS_CI=true \
|
||||
-e PG_MAJOR_VERSION=17 \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
-e LD_LIBRARY_PATH=/tmp/pg_upgrade_bin/17/lib \
|
||||
-e NIX_PGLIBDIR=/tmp/pg_upgrade_bin/17/lib \
|
||||
"$UPGRADE_CONTAINER" \
|
||||
/tmp/upgrade/initiate.sh 17; then
|
||||
echo ""
|
||||
warn "initiate.sh failed. Its cleanup may have restored the original state"
|
||||
warn "(re-enabled extensions, revoked superuser). Your data directory is"
|
||||
warn "unchanged - no data was moved or deleted."
|
||||
warn ""
|
||||
warn "Check the output above for the root cause, fix it, and re-run."
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "initiate.sh failed"
|
||||
fi
|
||||
|
||||
info "initiate.sh completed successfully"
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# --- Step 6: Run complete.sh in a native PG17 container -------------------
|
||||
#
|
||||
# complete.sh applies post-upgrade patches (pg_net grants, vault re-encryption,
|
||||
# pg_cron, predefined roles, vacuumdb, etc.). We run it in a PG17 container
|
||||
# where the binaries are native - no nix extraction or LD_LIBRARY_PATH needed.
|
||||
|
||||
run_complete() {
|
||||
local abs_migration_dir
|
||||
|
||||
abs_migration_dir=$(cd "$MIGRATION_DIR" && pwd)
|
||||
|
||||
info "Starting PG17 container for complete.sh"
|
||||
docker run -d --name "$COMPLETE_CONTAINER" \
|
||||
--entrypoint sleep \
|
||||
-v "${abs_migration_dir}:/mnt/host-migration" \
|
||||
-v "${db_config_vol}:/etc/postgresql-custom" \
|
||||
-v "${staging_dir}:/tmp/staging:ro" \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$PG17_UPGRADE_IMAGE" \
|
||||
infinity
|
||||
|
||||
info "Preparing complete.sh environment"
|
||||
# Save original db-config ownership so we can restore it if complete.sh fails.
|
||||
# complete.sh needs PG17 ownership to start postgres, but if it fails the
|
||||
# user needs to fall back to PG15 which uses a different uid.
|
||||
docker exec "$COMPLETE_CONTAINER" bash -c '
|
||||
stat -c "%u:%g" /etc/postgresql-custom/pgsodium_root.key 2>/dev/null > /tmp/dbconfig_owner || true
|
||||
'
|
||||
|
||||
docker exec "$COMPLETE_CONTAINER" bash -c '
|
||||
# Symlink bind mount so complete.sh CI wrapper can mv/rm/ln
|
||||
ln -s /mnt/host-migration /data_migration
|
||||
|
||||
# Remove the image default data dir (complete.sh creates a symlink here)
|
||||
rm -rf /var/lib/postgresql/data
|
||||
|
||||
# Fix ownership on db-config volume (PG15 uid differs from PG17)
|
||||
chown -R postgres:postgres /etc/postgresql-custom/
|
||||
|
||||
# PG17 config includes this directory; may not exist from PG15
|
||||
mkdir -p /etc/postgresql-custom/conf.d
|
||||
|
||||
mkdir -p /tmp/upgrade
|
||||
|
||||
# Copy upgrade scripts
|
||||
cp /tmp/staging/scripts/*.sh /tmp/upgrade/
|
||||
chmod +x /tmp/upgrade/*.sh
|
||||
|
||||
# Patch --new-bin to use native bindir (we are in a PG17 container,
|
||||
# no need for /tmp/pg_upgrade_bin/ paths)
|
||||
sed -i "s|BINDIR=\"/tmp/pg_upgrade_bin/\$PG_MAJOR_VERSION/bin\"|BINDIR=\$(pg_config --bindir)|g" /tmp/upgrade/common.sh
|
||||
'
|
||||
|
||||
info "Running complete.sh (post-upgrade patches, vacuum analyze)"
|
||||
docker exec \
|
||||
-e IS_CI=true \
|
||||
-e PG_MAJOR_VERSION=17 \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$COMPLETE_CONTAINER" \
|
||||
/tmp/upgrade/complete.sh || true
|
||||
|
||||
# complete.sh's ERR trap exits with 0 in some cases; check status file
|
||||
local status
|
||||
status=$(docker exec "$COMPLETE_CONTAINER" cat /tmp/pg-upgrade-status 2>/dev/null || echo "unknown")
|
||||
if [ "$status" != "complete" ]; then
|
||||
warn "complete.sh failed. Postgres log:"
|
||||
docker exec "$COMPLETE_CONTAINER" cat /tmp/postgres.log 2>/dev/null || true
|
||||
echo ""
|
||||
# Restore db-config ownership so PG15 can start for rollback
|
||||
warn "Restoring db-config ownership for PG15..."
|
||||
local orig_owner
|
||||
orig_owner=$(docker exec "$COMPLETE_CONTAINER" cat /tmp/dbconfig_owner 2>/dev/null || true)
|
||||
if [ -n "$orig_owner" ]; then
|
||||
docker exec "$COMPLETE_CONTAINER" chown -R "$orig_owner" /etc/postgresql-custom/ 2>/dev/null || true
|
||||
fi
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
echo ""
|
||||
echo " Your Postgres 15 data is unchanged (data swap has not happened yet)."
|
||||
echo " To restart Postgres 15:"
|
||||
echo " rm -rf $MIGRATION_DIR"
|
||||
echo " docker compose up -d"
|
||||
echo ""
|
||||
die "complete.sh failed (status: $status)"
|
||||
fi
|
||||
|
||||
info "complete.sh finished successfully"
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# --- Step 7: Swap data directories -----------------------------------------
|
||||
|
||||
swap_data() {
|
||||
info "Swapping data directories"
|
||||
|
||||
echo " $DATA_DIR -> $BACKUP_DIR"
|
||||
mv "$DATA_DIR" "$BACKUP_DIR"
|
||||
|
||||
echo " $MIGRATION_DIR/pgdata -> $DATA_DIR"
|
||||
mv "$MIGRATION_DIR/pgdata" "$DATA_DIR"
|
||||
rm -rf "$MIGRATION_DIR"
|
||||
}
|
||||
|
||||
# --- Step 8: Start Postgres 17 ---------------------------------------------
|
||||
|
||||
start_pg17() {
|
||||
info "Starting Supabase with Postgres 17"
|
||||
|
||||
# Ensure db-config volume has correct ownership and structure for PG17.
|
||||
# complete.sh does this too, but just in case of partial
|
||||
# failures from previous runs.
|
||||
docker run --rm -v "${db_config_vol}:/vol" "$PG17_TARGET_IMAGE" sh -c '
|
||||
mkdir -p /vol/conf.d
|
||||
chown -R postgres:postgres /vol/
|
||||
'
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d
|
||||
|
||||
echo " Waiting for Postgres 17 to be ready..."
|
||||
local retries=60
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec "$DB_CONTAINER" pg_isready -U postgres -h localhost >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 2
|
||||
done
|
||||
[ $retries -gt 0 ] || die "Postgres 17 did not start within 120 seconds."
|
||||
|
||||
local new_version
|
||||
new_version=$(run_sql_on "$DB_CONTAINER" -A -t -c "SHOW server_version;" 2>/dev/null | head -n 1)
|
||||
echo " Postgres version: $new_version"
|
||||
case "$new_version" in
|
||||
17.*) ;;
|
||||
*) die "Expected Postgres 17.x, got: $new_version" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Step 9: Apply migrations not covered by complete.sh -------------------
|
||||
#
|
||||
# These PG17 migrations run on fresh installs via initdb but not after
|
||||
# pg_upgrade (init scripts don't rerun when PG_VERSION already exists).
|
||||
# complete.sh doesn't cover them either.
|
||||
#
|
||||
# Source: postgres/migrations/db/migrations/
|
||||
# - 20250710151649_supabase_read_only_user_default_transaction_read_only.sql
|
||||
# - 20251001204436_predefined_role_grants.sql (supabase_etl_admin + pg_monitor)
|
||||
# - 20251105172723_grant_pg_reload_conf_to_postgres.sql
|
||||
# - 20251121132723_correct_search_path_pgbouncer.sql
|
||||
|
||||
apply_role_migrations() {
|
||||
info "Applying Postgres 17 migrations"
|
||||
|
||||
# Fix collation version mismatch first (upgrade used glibc 2.39, target
|
||||
# image may use glibc 2.40). Do this before any other SQL to suppress
|
||||
# the noisy warnings on every subsequent command.
|
||||
for db in postgres template1 _supabase; do
|
||||
docker exec -i -e PGPASSWORD="$pg_password" "$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d "$db" \
|
||||
-c "ALTER DATABASE \"$db\" REFRESH COLLATION VERSION;" || true
|
||||
done
|
||||
|
||||
# Create supabase_etl_admin role (doesn't exist in PG15 images).
|
||||
# Must be created before running predefined_role_grants.sql which
|
||||
# assumes it exists.
|
||||
run_sql_on "$DB_CONTAINER" -c "
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_etl_admin') THEN
|
||||
CREATE USER supabase_etl_admin WITH LOGIN REPLICATION;
|
||||
GRANT pg_read_all_data TO supabase_etl_admin;
|
||||
GRANT CREATE ON DATABASE postgres TO supabase_etl_admin;
|
||||
END IF;
|
||||
END
|
||||
\$\$;" || true
|
||||
|
||||
# Run the migration files directly from the PG17 container image.
|
||||
# They're idempotent (IF EXISTS / IF NOT EXISTS guards).
|
||||
local migration_dir="/docker-entrypoint-initdb.d/migrations"
|
||||
local migrations="
|
||||
20250710151649_supabase_read_only_user_default_transaction_read_only.sql
|
||||
20251001204436_predefined_role_grants.sql
|
||||
20251105172723_grant_pg_reload_conf_to_postgres.sql
|
||||
20251121132723_correct_search_path_pgbouncer.sql
|
||||
"
|
||||
|
||||
for m in $migrations; do
|
||||
echo " Running: $m"
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 \
|
||||
-f "${migration_dir}/${m}" || warn " $m failed (non-fatal)"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Step 10: Verify ------------------------------------------------------
|
||||
|
||||
verify() {
|
||||
info "Verification"
|
||||
|
||||
local version
|
||||
version=$(run_sql_on "$DB_CONTAINER" -A -t -c "SELECT version();" 2>/dev/null | head -n 1)
|
||||
echo " $version"
|
||||
|
||||
echo ""
|
||||
echo " Extensions:"
|
||||
run_sql_on "$DB_CONTAINER" -c \
|
||||
"SELECT extname, extversion FROM pg_extension ORDER BY extname;"
|
||||
|
||||
echo ""
|
||||
info "Upgrade complete!"
|
||||
echo ""
|
||||
echo " To use Postgres 17 going forward, always include the override:"
|
||||
echo " docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d"
|
||||
echo ""
|
||||
echo " Postgres 15 backup: $BACKUP_DIR"
|
||||
echo " pgsodium key backup: ./volumes/db/pgsodium_root.key.bak.pg15"
|
||||
echo " Once satisfied, you can reclaim space:"
|
||||
echo " rm -rf $BACKUP_DIR ./volumes/db/pg17_upgrade_bin_*.tar.gz"
|
||||
echo ""
|
||||
echo " Rollback (if needed):"
|
||||
echo " 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down"
|
||||
echo " 2. rm -rf $DATA_DIR"
|
||||
echo " 3. mv $BACKUP_DIR $DATA_DIR"
|
||||
echo " 4. docker compose run --rm db chown -R postgres:postgres /etc/postgresql-custom/"
|
||||
echo " 5. docker compose up -d"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# --- Main -------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
echo ""
|
||||
echo "Supabase Self-Hosted: Postgres 15 -> 17 Upgrade"
|
||||
echo "================================================"
|
||||
|
||||
preflight
|
||||
pull_image
|
||||
build_tarball
|
||||
drop_incompatible_extensions
|
||||
stop_and_backup
|
||||
run_upgrade
|
||||
run_complete
|
||||
swap_data
|
||||
start_pg17
|
||||
apply_role_migrations
|
||||
verify
|
||||
}
|
||||
|
||||
main "$@"
|
||||
135
versions.md
Normal file
135
versions.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Docker Image Versions
|
||||
|
||||
## 2026-06-03
|
||||
- supabase/studio:2026.06.03-sha-0bca601 (prev supabase/studio:2026.04.27-sha-5f60601)
|
||||
- supabase/gotrue:v2.189.0 (prev supabase/gotrue:v2.186.0)
|
||||
- postgrest/postgrest:v14.12 (prev postgrest/postgrest:v14.8)
|
||||
- supabase/realtime:v2.102.3 (prev supabase/realtime:v2.76.5)
|
||||
- supabase/storage-api:v1.60.4 (prev supabase/storage-api:v1.48.26)
|
||||
- supabase/postgres-meta:v0.96.6 (prev supabase/postgres-meta:v0.96.3)
|
||||
- supabase/edge-runtime:v1.74.0 (prev supabase/edge-runtime:v1.71.2)
|
||||
- supabase/supavisor:2.9.5 (prev supabase/supavisor:2.7.4)
|
||||
- supabase/logflare:1.43.1 (prev supabase/logflare:1.36.1)
|
||||
|
||||
## 2026-04-27
|
||||
- supabase/studio:2026.04.27-sha-5f60601 (prev supabase/studio:2026.04.08-sha-205cbe7)
|
||||
|
||||
## 2026-04-08
|
||||
- supabase/studio:2026.04.08-sha-205cbe7 (prev supabase/studio:2026.03.16-sha-5528817)
|
||||
- postgrest/postgrest:v14.8 (prev postgrest/postgrest:v14.6)
|
||||
- supabase/storage-api:v1.48.26 (prev supabase/storage-api:v1.44.2)
|
||||
- supabase/postgres-meta:v0.96.3 (prev supabase/postgres-meta:v0.95.2)
|
||||
- supabase/logflare:1.36.1 (prev supabase/logflare:1.31.2)
|
||||
|
||||
## 2026-03-16
|
||||
- supabase/studio:2026.03.16-sha-5528817 (prev supabase/studio:2026.02.16-sha-26c615c)
|
||||
- kong/kong:3.9.1 (prev kong:2.8.1)
|
||||
- postgrest/postgrest:v14.6 (prev postgrest/postgrest:v14.5)
|
||||
- supabase/storage-api:v1.44.2 (prev supabase/storage-api:v1.37.8)
|
||||
- supabase/edge-runtime:v1.71.2 (prev supabase/edge-runtime:v1.70.3)
|
||||
|
||||
## 2026-02-16
|
||||
- supabase/studio:2026.02.16-sha-26c615c (prev supabase/studio:2026.01.27-sha-6aa59ff)
|
||||
- supabase/gotrue:v2.186.0 (prev supabase/gotrue:v2.185.0)
|
||||
- postgrest/postgrest:v14.5 (prev postgrest/postgrest:v14.3)
|
||||
- supabase/realtime:v2.76.5 (prev supabase/realtime:v2.72.0)
|
||||
- supabase/storage-api:v1.37.8 (prev supabase/storage-api:v1.37.1)
|
||||
- supabase/edge-runtime:v1.70.3 (prev supabase/edge-runtime:v1.70.0)
|
||||
- supabase/logflare:1.31.2 (prev supabase/logflare:1.30.3)
|
||||
- timberio/vector:0.53.0-alpine (prev timberio/vector:0.28.1-alpine)
|
||||
|
||||
## 2026-02-05
|
||||
- supabase/storage-api:v1.37.1 (prev supabase/storage-api:v1.33.5)
|
||||
|
||||
## 2026-01-27
|
||||
- supabase/studio:2026.01.27-sha-6aa59ff (prev supabase/studio:2025.12.17-sha-43f4f7f)
|
||||
- supabase/gotrue:v2.185.0 (prev supabase/gotrue:v2.184.0)
|
||||
- postgrest/postgrest:v14.3 (prev postgrest/postgrest:v14.1)
|
||||
- supabase/realtime:v2.72.0 (prev supabase/realtime:v2.68.0)
|
||||
- supabase/storage-api:v1.33.5 (prev supabase/storage-api:v1.33.0)
|
||||
- darthsim/imgproxy:v3.30.1 (prev darthsim/imgproxy:v3.8.0)
|
||||
- supabase/postgres-meta:v0.95.2 (prev supabase/postgres-meta:v0.95.1)
|
||||
- supabase/edge-runtime:v1.70.0 (prev supabase/edge-runtime:v1.69.28)
|
||||
- supabase/logflare:1.30.3 (prev supabase/logflare:1.27.0)
|
||||
|
||||
## 2025-12-18
|
||||
- supabase/studio:2025.12.17-sha-43f4f7f (prev supabase/studio:2025.12.09-sha-434634f)
|
||||
- supabase/gotrue:v2.184.0 (prev supabase/gotrue:v2.183.0)
|
||||
- supabase/postgres-meta:v0.95.1 (prev supabase/postgres-meta:v0.93.1)
|
||||
- supabase/logflare:1.27.0 (prev supabase/logflare:1.26.25)
|
||||
|
||||
## 2025-12-10
|
||||
- supabase/studio:2025.12.09-sha-434634f (prev supabase/studio:2025.11.26-sha-8f096b5)
|
||||
- postgrest/postgrest:v14.1 (prev postgrest/postgrest:v13.0.7)
|
||||
- supabase/realtime:v2.68.0 (prev supabase/realtime:v2.65.3)
|
||||
- supabase/storage-api:v1.33.0 (prev supabase/storage-api:v1.32.0)
|
||||
- supabase/edge-runtime:v1.69.28 (prev supabase/edge-runtime:v1.69.25)
|
||||
- supabase/logflare:1.26.25 (prev supabase/logflare:1.26.13)
|
||||
|
||||
## 2025-11-26
|
||||
- supabase/studio:2025.11.26-sha-8f096b5 (prev supabase/studio:2025.11.24-sha-d990ae8)
|
||||
- supabase/realtime:v2.65.3 (prev supabase/realtime:v2.65.2)
|
||||
- supabase/logflare:1.26.13 (prev supabase/logflare:1.26.12)
|
||||
|
||||
## 2025-11-25
|
||||
- supabase/studio:2025.11.24-sha-d990ae8 (prev supabase/studio:2025.11.10-sha-5291fe3)
|
||||
- supabase/gotrue:v2.183.0 (prev supabase/gotrue:v2.182.1)
|
||||
- supabase/realtime:v2.65.2 (prev supabase/realtime:v2.63.0)
|
||||
- supabase/storage-api:v1.32.0 (prev supabase/storage-api:v1.29.0)
|
||||
- supabase/edge-runtime:v1.69.25 (prev supabase/edge-runtime:v1.69.23)
|
||||
- supabase/logflare:1.26.12 (prev supabase/logflare:1.22.6)
|
||||
|
||||
## 2025-11-12
|
||||
- supabase/studio:2025.11.10-sha-5291fe3 (prev supabase/studio:2025.10.27-sha-85b84e0)
|
||||
- supabase/gotrue:v2.182.1 (prev supabase/gotrue:v2.180.0)
|
||||
- supabase/realtime:v2.63.0 (prev supabase/realtime:v2.57.2)
|
||||
- supabase/storage-api:v1.29.0 (prev supabase/storage-api:v1.28.2)
|
||||
- supabase/edge-runtime:v1.69.23 (prev supabase/edge-runtime:v1.69.15)
|
||||
- supabase/supavisor:2.7.4 (prev supabase/supavisor:2.7.3)
|
||||
|
||||
## 2025-10-28
|
||||
- supabase/studio:2025.10.27-sha-85b84e0 (prev supabase/studio:2025.10.20-sha-5005fc6)
|
||||
- supabase/realtime:v2.57.2 (prev supabase/realtime:v2.56.0)
|
||||
- supabase/storage-api:v1.28.2 (prev supabase/storage-api:v1.28.1)
|
||||
- supabase/postgres-meta:v0.93.1 (prev supabase/postgres-meta:v0.93.0)
|
||||
- supabase/edge-runtime:v1.69.15 (prev supabase/edge-runtime:v1.69.14)
|
||||
|
||||
## 2025-10-21
|
||||
- supabase/studio:2025.10.20-sha-5005fc6 (prev supabase/studio:2025.10.01-sha-8460121)
|
||||
- supabase/realtime:v2.56.0 (prev supabase/realtime:v2.51.11)
|
||||
- supabase/storage-api:v1.28.1 (prev supabase/storage-api:v1.28.0)
|
||||
- supabase/postgres-meta:v0.93.0 (prev supabase/postgres-meta:v0.91.6)
|
||||
- supabase/edge-runtime:v1.69.14 (prev supabase/edge-runtime:v1.69.6)
|
||||
- supabase/supavisor:2.7.3 (prev supabase/supavisor:2.7.0)
|
||||
|
||||
## 2025-10-13
|
||||
- supabase/logflare:1.22.6 (prev supabase/logflare:1.22.4)
|
||||
|
||||
## 2025-10-08
|
||||
- supabase/studio:2025.10.01-sha-8460121 (prev supabase/studio:2025.06.30-sha-6f5982d)
|
||||
- supabase/gotrue:v2.180.0 (prev supabase/gotrue:v2.177.0)
|
||||
- postgrest/postgrest:v13.0.7 (prev postgrest/postgrest:v12.2.12)
|
||||
- supabase/realtime:v2.51.11 (prev supabase/realtime:v2.34.47)
|
||||
- supabase/storage-api:v1.28.0 (prev supabase/storage-api:v1.25.7)
|
||||
- supabase/postgres-meta:v0.91.6 (prev supabase/postgres-meta:v0.91.0)
|
||||
- supabase/logflare:1.22.4 (prev supabase/logflare:1.14.2)
|
||||
- supabase/postgres:15.8.1.085 (prev supabase/postgres:15.8.1.060)
|
||||
- supabase/supavisor:2.7.0 (prev supabase/supavisor:2.5.7)
|
||||
|
||||
## 2025-07-15
|
||||
- supabase/gotrue:v2.177.0 (prev supabase/gotrue:v2.176.1)
|
||||
- supabase/storage-api:v1.25.7 (prev supabase/storage-api:v1.24.7)
|
||||
- supabase/postgres-meta:v0.91.0 (prev supabase/postgres-meta:v0.89.3)
|
||||
- supabase/supavisor:2.5.7 (prev supabase/supavisor:2.5.6)
|
||||
|
||||
## 2025-07-02
|
||||
- supabase/studio:2025.06.30-sha-6f5982d (prev supabase/studio:2025.06.02-sha-8f2993d)
|
||||
- supabase/gotrue:v2.176.1 (prev supabase/gotrue:v2.174.0)
|
||||
- supabase/storage-api:v1.24.7 (prev supabase/storage-api:v1.23.0)
|
||||
- supabase/supavisor:2.5.6 (prev supabase/supavisor:2.5.1)
|
||||
|
||||
## 2025-06-03
|
||||
- supabase/studio:2025.06.02-sha-8f2993d (prev supabase/studio:2025.05.19-sha-3487831)
|
||||
- supabase/gotrue:v2.174.0 (prev supabase/gotrue:v2.172.1)
|
||||
- supabase/storage-api:v1.23.0 (prev supabase/storage-api:v1.22.17)
|
||||
- supabase/postgres-meta:v0.89.3 (prev supabase/postgres-meta:v0.89.0)
|
||||
223
volumes/api/envoy/cds.yaml
Normal file
223
volumes/api/envoy/cds.yaml
Normal file
@@ -0,0 +1,223 @@
|
||||
resources:
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: auth
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: auth
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: auth
|
||||
port_value: 9999
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: rest
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: rest
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: rest
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: realtime
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: realtime
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: realtime-dev.supabase-realtime
|
||||
port_value: 4000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: storage
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: storage
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: storage
|
||||
port_value: 5000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /status
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: functions
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: functions
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: functions
|
||||
port_value: 9000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
tcp_health_check: {}
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: meta
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: meta
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: meta
|
||||
port_value: 8080
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: studio
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: studio
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: studio
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /project/default
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
34
volumes/api/envoy/docker-entrypoint.sh
Executable file
34
volumes/api/envoy/docker-entrypoint.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Generate SHA1 base64 hash for Envoy basic auth user list
|
||||
PASSWORD_HASH=$(printf '%s' "${DASHBOARD_PASSWORD}" | openssl sha1 -binary | openssl base64)
|
||||
DASHBOARD_BASIC_AUTH="${DASHBOARD_USERNAME}:{SHA}${PASSWORD_HASH}"
|
||||
|
||||
echo "Generating Envoy configuration..."
|
||||
|
||||
# Process the lds.yaml template with environment variables using sed
|
||||
# Using | as delimiter since JWT tokens contain /
|
||||
sed -e "s|\${ANON_KEY}|${ANON_KEY}|g" \
|
||||
-e "s|\${ANON_KEY_ASYMMETRIC}|${ANON_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY}|${SERVICE_ROLE_KEY}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY_ASYMMETRIC}|${SERVICE_ROLE_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SUPABASE_PUBLISHABLE_KEY}|${SUPABASE_PUBLISHABLE_KEY}|g" \
|
||||
-e "s|\${SUPABASE_SECRET_KEY}|${SUPABASE_SECRET_KEY}|g" \
|
||||
-e "s|\${DASHBOARD_BASIC_AUTH}|${DASHBOARD_BASIC_AUTH}|g" \
|
||||
/etc/envoy/lds.template.yaml > /etc/envoy/lds.yaml
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && \
|
||||
[ -n "$SUPABASE_PUBLISHABLE_KEY" ] && \
|
||||
[ -n "$SERVICE_ROLE_KEY_ASYMMETRIC" ] && \
|
||||
[ -n "$ANON_KEY_ASYMMETRIC" ]; then
|
||||
echo "Envoy sb_ key translation enabled"
|
||||
else
|
||||
echo "Envoy running in legacy API key mode (sb_ keys disabled)"
|
||||
fi
|
||||
|
||||
echo "Envoy configuration generated successfully"
|
||||
echo "Starting Envoy..."
|
||||
|
||||
# Start Envoy
|
||||
exec envoy -c /etc/envoy/envoy.yaml "$@"
|
||||
27
volumes/api/envoy/envoy.yaml
Normal file
27
volumes/api/envoy/envoy.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
dynamic_resources:
|
||||
cds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/cds.yaml
|
||||
resource_api_version: V3
|
||||
lds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/lds.yaml
|
||||
resource_api_version: V3
|
||||
|
||||
node:
|
||||
cluster: supabase_cluster
|
||||
id: supabase_node
|
||||
|
||||
overload_manager:
|
||||
resource_monitors:
|
||||
- name: envoy.resource_monitors.global_downstream_max_connections
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig
|
||||
max_active_downstream_connections: 30000
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
address: 127.0.0.1
|
||||
port_value: 9901
|
||||
994
volumes/api/envoy/lds.template.yaml
Normal file
994
volumes/api/envoy/lds.template.yaml
Normal file
@@ -0,0 +1,994 @@
|
||||
resources:
|
||||
- '@type': type.googleapis.com/envoy.config.listener.v3.Listener
|
||||
name: supabase
|
||||
per_connection_buffer_limit_bytes: 32768 # 32 KiB
|
||||
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8000
|
||||
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
stat_prefix: ingress_http
|
||||
normalize_path: true
|
||||
merge_slashes: true
|
||||
path_with_escaped_slashes_action: REJECT_REQUEST
|
||||
use_remote_address: true
|
||||
common_http_protocol_options:
|
||||
headers_with_underscores_action: REJECT_REQUEST
|
||||
upgrade_configs:
|
||||
- upgrade_type: websocket
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
log_format:
|
||||
text_format_source:
|
||||
inline_string: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% - - [%START_TIME(%d/%b/%Y:%H:%M:%S %z)%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %BYTES_SENT% \"%REQ(REFERER)%\" \"%REQ(USER-AGENT)%\"\n"
|
||||
|
||||
route_config:
|
||||
name: supabase_route
|
||||
virtual_hosts:
|
||||
- name: supabase_host
|
||||
domains:
|
||||
- '*'
|
||||
cors:
|
||||
allow_origin_string_match:
|
||||
- safe_regex:
|
||||
regex: ".*"
|
||||
allow_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD,CONNECT,TRACE"
|
||||
allow_headers: "*"
|
||||
expose_headers: "*"
|
||||
max_age: "3600"
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Host
|
||||
value: "%REQ(:AUTHORITY)%"
|
||||
append_action: ADD_IF_ABSENT
|
||||
- header:
|
||||
key: X-Forwarded-Port
|
||||
value: "%DOWNSTREAM_LOCAL_PORT%"
|
||||
append_action: ADD_IF_ABSENT
|
||||
routes:
|
||||
- match:
|
||||
prefix: /auth/v1/verify
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /verify
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/verify
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/callback
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /callback
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/callback
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/authorize
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /authorize
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/authorize
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/.well-known/jwks.json
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /.well-known/jwks.json
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/.well-known/jwks.json
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /.well-known/oauth-authorization-server
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /.well-known/oauth-authorization-server
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /sso/saml/acs
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /sso/saml/acs
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /sso/saml/metadata
|
||||
route:
|
||||
cluster: auth
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /sso/saml/metadata
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: functions-v1-all
|
||||
match:
|
||||
prefix: /functions/v1/
|
||||
route:
|
||||
cluster: functions
|
||||
prefix_rewrite: /
|
||||
timeout: 150s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /functions/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /storage/v1/
|
||||
route:
|
||||
cluster: storage
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /storage/v1
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: auth-v1-protected
|
||||
match:
|
||||
prefix: /auth/v1/
|
||||
route:
|
||||
cluster: auth
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /auth/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: rest-v1-protected
|
||||
match:
|
||||
prefix: /rest/v1/
|
||||
route:
|
||||
cluster: rest
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /rest/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: graphql-v1-protected
|
||||
match:
|
||||
prefix: /graphql/v1
|
||||
route:
|
||||
cluster: rest
|
||||
prefix_rewrite: /rpc/graphql
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /graphql/v1
|
||||
append_action: ADD_IF_ABSENT
|
||||
- header:
|
||||
key: Content-Profile
|
||||
value: graphql_public
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: realtime-v1-api-protected
|
||||
match:
|
||||
prefix: /realtime/v1/api
|
||||
route:
|
||||
cluster: realtime
|
||||
prefix_rewrite: /api
|
||||
timeout: 30s
|
||||
host_rewrite_literal: realtime-dev.supabase-realtime
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/api
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: realtime-v1-ws-protected
|
||||
match:
|
||||
prefix: /realtime/v1/
|
||||
route:
|
||||
cluster: realtime
|
||||
prefix_rewrite: /socket/
|
||||
timeout: 30s
|
||||
host_rewrite_literal: realtime-dev.supabase-realtime
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- name: pg-protected
|
||||
match:
|
||||
prefix: /pg/
|
||||
route:
|
||||
cluster: meta
|
||||
prefix_rewrite: /
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /pg/
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
- match:
|
||||
prefix: /api/mcp
|
||||
route:
|
||||
cluster: studio
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /api/mcp
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- match:
|
||||
prefix: /mcp
|
||||
route:
|
||||
cluster: studio
|
||||
prefix_rewrite: /api/mcp
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /mcp
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
# Block access to /mcp by default
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'rbac' block above.
|
||||
# 2. Uncomment and adjust the 'rbac' block below.
|
||||
# 3. Add or adjust your local IPs in 'principals'.
|
||||
#rbac:
|
||||
# rules:
|
||||
# action: ALLOW
|
||||
# policies:
|
||||
# allow_local:
|
||||
# permissions:
|
||||
# - any: true
|
||||
# principals:
|
||||
# - direct_remote_ip:
|
||||
# address_prefix: 127.0.0.1
|
||||
# prefix_len: 32
|
||||
# - direct_remote_ip:
|
||||
# address_prefix: ::1
|
||||
# prefix_len: 128
|
||||
|
||||
- match:
|
||||
prefix: /
|
||||
route:
|
||||
cluster: studio
|
||||
timeout: 30s
|
||||
request_headers_to_remove:
|
||||
- authorization
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
allow_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
http_filters:
|
||||
- name: envoy.filters.http.cors
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
|
||||
|
||||
- name: envoy.filters.http.basic_auth
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
|
||||
users:
|
||||
inline_string: '${DASHBOARD_BASIC_AUTH}'
|
||||
|
||||
# Copies ?apikey=... from the URL into the apikey header when clients omit the header.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
if headers:get("apikey") ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local query = string.sub(path, query_start + 1)
|
||||
for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do
|
||||
if key == "apikey" and value ~= "" then
|
||||
headers:add("apikey", value)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns 401 for missing/invalid API keys on protected API routes.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local ANON_KEY = "${ANON_KEY}"
|
||||
local SERVICE_ROLE_KEY = "${SERVICE_ROLE_KEY}"
|
||||
local SUPABASE_PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SUPABASE_SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local ANON_KEY_ASYMMETRIC = "${ANON_KEY_ASYMMETRIC}"
|
||||
local SERVICE_ROLE_KEY_ASYMMETRIC = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SUPABASE_SECRET_KEY ~= "" and SUPABASE_PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_KEY_ASYMMETRIC ~= "" and ANON_KEY_ASYMMETRIC ~= ""
|
||||
|
||||
local PROTECTED_ROUTES = {
|
||||
["auth-v1-protected"] = true,
|
||||
["rest-v1-protected"] = true,
|
||||
["graphql-v1-protected"] = true,
|
||||
["realtime-v1-api-protected"] = true,
|
||||
["realtime-v1-ws-protected"] = true,
|
||||
["pg-protected"] = true,
|
||||
}
|
||||
|
||||
local function is_protected_route(route_name)
|
||||
if route_name == nil or route_name == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
return PROTECTED_ROUTES[route_name] == true
|
||||
end
|
||||
|
||||
local function is_valid_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if SERVICE_ROLE_KEY ~= "" and apikey == SERVICE_ROLE_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if ANON_KEY ~= "" and apikey == ANON_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if TRANSLATION_ENABLED and apikey == SUPABASE_SECRET_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
if TRANSLATION_ENABLED and apikey == SUPABASE_PUBLISHABLE_KEY then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
local route_name = request_handle:streamInfo():routeName()
|
||||
if not is_protected_route(route_name) then
|
||||
return
|
||||
end
|
||||
|
||||
if is_valid_apikey(headers:get("apikey")) then
|
||||
return
|
||||
end
|
||||
|
||||
request_handle:respond({
|
||||
[":status"] = "401",
|
||||
["content-type"] = "text/plain",
|
||||
}, "Unauthorized")
|
||||
end
|
||||
|
||||
# Translates the query parameter apikey into the matching internal JWT and rewrites the URL so only JWTs propagate downstream.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local ANON_JWT = "${ANON_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= ""
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function translate_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not TRANSLATION_ENABLED then
|
||||
return nil
|
||||
end
|
||||
|
||||
if apikey == SECRET_KEY then
|
||||
return SERVICE_ROLE_JWT
|
||||
end
|
||||
|
||||
if apikey == PUBLISHABLE_KEY then
|
||||
return ANON_JWT
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function extract_query_apikey(path)
|
||||
if path == nil or path == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query = string.sub(path, query_start + 1)
|
||||
for key, value in string.gmatch(query, "([^&]+)=([^&]*)") do
|
||||
if key == "apikey" and value ~= "" then
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function replace_query_apikey(path, new_value)
|
||||
if path == nil or path == "" or new_value == nil or new_value == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local query_start = string.find(path, "?", 1, true)
|
||||
if query_start == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local base = string.sub(path, 1, query_start)
|
||||
local query = string.sub(path, query_start + 1)
|
||||
local updated = {}
|
||||
local replaced = false
|
||||
|
||||
for part in string.gmatch(query, "([^&]+)") do
|
||||
local key, value = string.match(part, "([^=]+)=(.*)")
|
||||
if key == "apikey" then
|
||||
part = key .. "=" .. new_value
|
||||
replaced = true
|
||||
end
|
||||
table.insert(updated, part)
|
||||
end
|
||||
|
||||
if not replaced then
|
||||
return nil
|
||||
end
|
||||
|
||||
return base .. table.concat(updated, "&")
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
local apikey = extract_query_apikey(path)
|
||||
local translated = translate_apikey(apikey)
|
||||
|
||||
if translated == nil then
|
||||
return
|
||||
end
|
||||
|
||||
headers:replace("apikey", translated)
|
||||
|
||||
local rewritten_path = replace_query_apikey(path, translated)
|
||||
if rewritten_path ~= nil then
|
||||
headers:replace(":path", rewritten_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Translates an apikey header into the appropriate internal JWT for downstream RBAC checks.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local SECRET_KEY = "${SUPABASE_SECRET_KEY}"
|
||||
local PUBLISHABLE_KEY = "${SUPABASE_PUBLISHABLE_KEY}"
|
||||
local SERVICE_ROLE_JWT = "${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
local ANON_JWT = "${ANON_KEY_ASYMMETRIC}"
|
||||
local TRANSLATION_ENABLED = SECRET_KEY ~= "" and PUBLISHABLE_KEY ~= "" and SERVICE_ROLE_JWT ~= "" and ANON_JWT ~= ""
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function translate_apikey(apikey)
|
||||
if apikey == nil or apikey == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not TRANSLATION_ENABLED then
|
||||
return nil
|
||||
end
|
||||
|
||||
if apikey == SECRET_KEY then
|
||||
return SERVICE_ROLE_JWT
|
||||
end
|
||||
|
||||
if apikey == PUBLISHABLE_KEY then
|
||||
return ANON_JWT
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
local translated = translate_apikey(headers:get("apikey"))
|
||||
if translated ~= nil and translated ~= "" then
|
||||
headers:replace("apikey", translated)
|
||||
end
|
||||
end
|
||||
|
||||
# Mirrors apikey into x-api-key for realtime WS compatibility.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local REALTIME_WS_ROUTE = "realtime-v1-ws-protected"
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local route_name = request_handle:streamInfo():routeName()
|
||||
if route_name ~= REALTIME_WS_ROUTE then
|
||||
return
|
||||
end
|
||||
|
||||
local headers = request_handle:headers()
|
||||
local apikey = headers:get("apikey")
|
||||
if apikey == nil or apikey == "" then
|
||||
return
|
||||
end
|
||||
|
||||
headers:replace("x-api-key", apikey)
|
||||
end
|
||||
|
||||
# Synthesizes an Authorization header (Bearer …) from apikey when callers don’t provide a real JWT header.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
|
||||
inline_code: |
|
||||
local FUNCTIONS_ROUTE = "functions-v1-all"
|
||||
local FUNCTIONS_PREFIX = "/functions/v1/"
|
||||
local REALTIME_WS_ROUTE = "realtime-v1-ws-protected"
|
||||
|
||||
local function is_functions_request(request_handle, headers)
|
||||
if request_handle:streamInfo():routeName() == FUNCTIONS_ROUTE then
|
||||
return true
|
||||
end
|
||||
|
||||
local path = headers:get(":path")
|
||||
if path == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(path, 1, string.len(FUNCTIONS_PREFIX)) == FUNCTIONS_PREFIX
|
||||
end
|
||||
|
||||
local function has_real_jwt(auth_header)
|
||||
if auth_header == nil or auth_header == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if string.sub(auth_header, 1, 7) ~= "Bearer " then
|
||||
return false
|
||||
end
|
||||
|
||||
return string.sub(auth_header, 1, 10) ~= "Bearer sb_"
|
||||
end
|
||||
|
||||
local function format_authorization(value)
|
||||
if value == nil or value == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
if string.sub(value, 1, 7) == "Bearer " then
|
||||
return value
|
||||
end
|
||||
|
||||
return "Bearer " .. value
|
||||
end
|
||||
|
||||
function envoy_on_request(request_handle)
|
||||
local headers = request_handle:headers()
|
||||
if request_handle:streamInfo():routeName() == REALTIME_WS_ROUTE then
|
||||
return
|
||||
end
|
||||
|
||||
if is_functions_request(request_handle, headers) then
|
||||
return
|
||||
end
|
||||
|
||||
if has_real_jwt(headers:get("authorization")) then
|
||||
return
|
||||
end
|
||||
|
||||
local apikey = headers:get("apikey")
|
||||
local authorization_value = format_authorization(apikey)
|
||||
if authorization_value ~= nil then
|
||||
headers:replace("authorization", authorization_value)
|
||||
end
|
||||
end
|
||||
|
||||
- name: envoy.filters.http.rbac
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
|
||||
rules:
|
||||
action: ALLOW
|
||||
policies:
|
||||
admin:
|
||||
permissions:
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /pg/
|
||||
principals:
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}'
|
||||
apikey:
|
||||
permissions:
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /auth/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /rest/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /realtime/v1/api
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /realtime/v1/
|
||||
- url_path:
|
||||
path:
|
||||
prefix: /graphql/v1
|
||||
principals:
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${ANON_KEY}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${SERVICE_ROLE_KEY_ASYMMETRIC}'
|
||||
- header:
|
||||
name: apikey
|
||||
string_match:
|
||||
exact: '${ANON_KEY_ASYMMETRIC}'
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
49
volumes/api/kong-entrypoint.sh
Executable file
49
volumes/api/kong-entrypoint.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||
# and performs environment variable substitution in the declarative config.
|
||||
|
||||
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||
# to legacy-only behavior - just passing apikey as-is.
|
||||
#
|
||||
# Full expression logic (when opaque keys are configured):
|
||||
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# Opaque keys configured -> full translation expressions
|
||||
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)"
|
||||
|
||||
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||
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
|
||||
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||
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
|
||||
|
||||
# Substitute environment variables in the Kong declarative config.
|
||||
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||
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"
|
||||
|
||||
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
exec /entrypoint.sh kong docker-start
|
||||
408
volumes/api/kong.yml
Normal file
408
volumes/api/kong.yml
Normal file
@@ -0,0 +1,408 @@
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
###
|
||||
### Consumers / Users
|
||||
###
|
||||
consumers:
|
||||
- username: DASHBOARD
|
||||
- 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
|
||||
###
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
###
|
||||
### Dashboard credentials
|
||||
###
|
||||
basicauth_credentials:
|
||||
- consumer: DASHBOARD
|
||||
username: '$DASHBOARD_USERNAME'
|
||||
password: '$DASHBOARD_PASSWORD'
|
||||
|
||||
###
|
||||
### API Routes
|
||||
###
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||
url: http://auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-jwks
|
||||
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||
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
|
||||
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- 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
|
||||
|
||||
## Secure PostgREST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- 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
|
||||
|
||||
## Secure GraphQL routes
|
||||
- name: graphql-v1
|
||||
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||
url: http://rest:3000/rpc/graphql
|
||||
routes:
|
||||
- name: graphql-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Content-Profile: graphql_public"
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure Realtime routes
|
||||
- name: realtime-v1-ws
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||
protocol: ws
|
||||
routes:
|
||||
- name: realtime-v1-ws
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- 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
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
- 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 API endpoint (with Authorization header transformation).
|
||||
## No key-auth — S3 protocol requests don't carry an apikey header.
|
||||
##
|
||||
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||
## The post-function strips this empty header so Storage's S3 signature
|
||||
## verification falls through to query-parameter parsing.
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /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
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
- name: well-known-oauth
|
||||
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||
routes:
|
||||
- name: well-known-oauth
|
||||
strip_path: true
|
||||
paths:
|
||||
- /.well-known/oauth-authorization-server
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||
# - name: analytics-v1-api
|
||||
# _comment: 'Analytics: /analytics/v1/api/endpoints/* -> http://logflare:4000/api/endpoints/*'
|
||||
# url: http://analytics:4000/api/endpoints
|
||||
# routes:
|
||||
# - name: analytics-v1-api
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1/api/endpoints/
|
||||
# - name: analytics-v1
|
||||
# _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
# url: http://analytics:4000/
|
||||
# routes:
|
||||
# - name: dashboard-v1-all
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1
|
||||
# plugins:
|
||||
# - name: cors
|
||||
# - name: basic-auth
|
||||
# config:
|
||||
# hide_credentials: true
|
||||
|
||||
## Secure Database routes
|
||||
- name: meta
|
||||
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||
url: http://meta:8080/
|
||||
routes:
|
||||
- name: meta-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Block access to /api/mcp
|
||||
- name: mcp-blocker
|
||||
_comment: 'Block direct access to /api/mcp'
|
||||
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."
|
||||
|
||||
## MCP endpoint - local access
|
||||
- name: mcp
|
||||
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp
|
||||
strip_path: true
|
||||
paths:
|
||||
- /mcp
|
||||
plugins:
|
||||
# Block access to /mcp by default
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'request-termination' section above
|
||||
# 2. Uncomment the entire section below, including 'deny'
|
||||
# 3. Add your local IPs to the 'allow' list
|
||||
#- name: cors
|
||||
#- name: ip-restriction
|
||||
# config:
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - ::1
|
||||
# deny: []
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
- name: dashboard
|
||||
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||
url: http://studio:3000/
|
||||
routes:
|
||||
- name: dashboard-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: basic-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
3
volumes/db/_supabase.sql
Normal file
3
volumes/db/_supabase.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||
0
volumes/db/init/data.sql
Executable file
0
volumes/db/init/data.sql
Executable file
5
volumes/db/jwt.sql
Normal file
5
volumes/db/jwt.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
\set jwt_secret `echo "$JWT_SECRET"`
|
||||
\set jwt_exp `echo "$JWT_EXP"`
|
||||
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
|
||||
6
volumes/db/logs.sql
Normal file
6
volumes/db/logs.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _analytics;
|
||||
alter schema _analytics owner to :pguser;
|
||||
\c postgres
|
||||
6
volumes/db/pooler.sql
Normal file
6
volumes/db/pooler.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _supavisor;
|
||||
alter schema _supavisor owner to :pguser;
|
||||
\c postgres
|
||||
4
volumes/db/realtime.sql
Normal file
4
volumes/db/realtime.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
create schema if not exists _realtime;
|
||||
alter schema _realtime owner to :pguser;
|
||||
8
volumes/db/roles.sql
Normal file
8
volumes/db/roles.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- NOTE: change to your own passwords for production environments
|
||||
\set pgpass `echo "$POSTGRES_PASSWORD"`
|
||||
|
||||
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
||||
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
|
||||
208
volumes/db/webhooks.sql
Normal file
208
volumes/db/webhooks.sql
Normal file
@@ -0,0 +1,208 @@
|
||||
BEGIN;
|
||||
-- Create pg_net extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
||||
-- Create supabase_functions schema
|
||||
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
|
||||
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
|
||||
-- supabase_functions.migrations definition
|
||||
CREATE TABLE supabase_functions.migrations (
|
||||
version text PRIMARY KEY,
|
||||
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Initial supabase_functions migration
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
||||
-- supabase_functions.hooks definition
|
||||
CREATE TABLE supabase_functions.hooks (
|
||||
id bigserial PRIMARY KEY,
|
||||
hook_table_id integer NOT NULL,
|
||||
hook_name text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
request_id bigint
|
||||
);
|
||||
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
||||
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
||||
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||
CREATE FUNCTION supabase_functions.http_request()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
request_id bigint;
|
||||
payload jsonb;
|
||||
url text := TG_ARGV[0]::text;
|
||||
method text := TG_ARGV[1]::text;
|
||||
headers jsonb DEFAULT '{}'::jsonb;
|
||||
params jsonb DEFAULT '{}'::jsonb;
|
||||
timeout_ms integer DEFAULT 1000;
|
||||
BEGIN
|
||||
IF url IS NULL OR url = 'null' THEN
|
||||
RAISE EXCEPTION 'url argument is missing';
|
||||
END IF;
|
||||
|
||||
IF method IS NULL OR method = 'null' THEN
|
||||
RAISE EXCEPTION 'method argument is missing';
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||
ELSE
|
||||
headers = TG_ARGV[2]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||
params = '{}'::jsonb;
|
||||
ELSE
|
||||
params = TG_ARGV[3]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||
timeout_ms = 1000;
|
||||
ELSE
|
||||
timeout_ms = TG_ARGV[4]::integer;
|
||||
END IF;
|
||||
|
||||
CASE
|
||||
WHEN method = 'GET' THEN
|
||||
SELECT http_get INTO request_id FROM net.http_get(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
WHEN method = 'POST' THEN
|
||||
payload = jsonb_build_object(
|
||||
'old_record', OLD,
|
||||
'record', NEW,
|
||||
'type', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA
|
||||
);
|
||||
|
||||
SELECT http_post INTO request_id FROM net.http_post(
|
||||
url,
|
||||
payload,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||
END CASE;
|
||||
|
||||
INSERT INTO supabase_functions.hooks
|
||||
(hook_table_id, hook_name, request_id)
|
||||
VALUES
|
||||
(TG_RELID, TG_NAME, request_id);
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$function$;
|
||||
-- Supabase super admin
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_functions_admin'
|
||||
)
|
||||
THEN
|
||||
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
||||
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
||||
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
||||
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
||||
GRANT supabase_functions_admin TO postgres;
|
||||
-- Remove unused supabase_pg_net_admin role
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_pg_net_admin'
|
||||
)
|
||||
THEN
|
||||
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
||||
DROP OWNED BY supabase_pg_net_admin;
|
||||
DROP ROLE supabase_pg_net_admin;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- pg_net grants when extension is already enabled
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_extension
|
||||
WHERE extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- Event trigger for pg_net
|
||||
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger
|
||||
WHERE evtname = 'issue_pg_net_access'
|
||||
) THEN
|
||||
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
||||
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
||||
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
||||
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
||||
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
|
||||
COMMIT;
|
||||
14
volumes/functions/hello/index.ts
Normal file
14
volumes/functions/hello/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
Deno.serve(async () => {
|
||||
return new Response(
|
||||
`"Hello from Edge Functions!"`,
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl 'http://localhost:<KONG_HTTP_PORT>/functions/v1/hello' \
|
||||
// --header 'Authorization: Bearer <anon/service_role API key>'
|
||||
168
volumes/functions/main/index.ts
Normal file
168
volumes/functions/main/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
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'
|
||||
|
||||
// Create JWKS for ES256/RS256 tokens (newer tokens)
|
||||
let SUPABASE_JWT_KEYS: ReturnType<typeof jose.createRemoteJWKSet> | 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JWT token from Authorization header
|
||||
*
|
||||
* Parses the Authorization header to extract the Bearer token.
|
||||
* Expects format: "Bearer <token>"
|
||||
*
|
||||
* @param req - The HTTP request object
|
||||
* @returns The JWT token string
|
||||
* @throws Error if Authorization header is missing or malformed
|
||||
*/
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get('authorization')
|
||||
if (!authHeader) {
|
||||
throw new Error('Missing authorization header')
|
||||
}
|
||||
const [bearer, token] = authHeader.split(' ')
|
||||
if (bearer !== 'Bearer') {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
async function isValidLegacyJWT(jwt: string): Promise<boolean> {
|
||||
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 (e) {
|
||||
console.error('Symmetric Legacy JWT verification error', e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function isValidJWT(jwt: string): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify JWT token, handling both legacy (HS256) and newer (ES256/RS256) algorithms
|
||||
*
|
||||
* This function automatically detects the algorithm used in the token and applies
|
||||
* the appropriate verification method:
|
||||
* - HS256: Uses JWT_SECRET (symmetric key)
|
||||
* - ES256/RS256: Uses JWKS endpoint (asymmetric public keys)
|
||||
*
|
||||
* This fix ensures compatibility with both legacy tokens and newer asymmetric tokens,
|
||||
* resolving the "Key for the ES256 algorithm must be of type CryptoKey" error.
|
||||
*
|
||||
* @param jwt - The JWT token string to verify
|
||||
* @returns Promise resolving to true if verification succeeds, false otherwise
|
||||
*/
|
||||
async function isValidHybridJWT(jwt: string): Promise<boolean> {
|
||||
const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt)
|
||||
|
||||
if (jwtAlgorithm === 'HS256') {
|
||||
console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`)
|
||||
|
||||
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 isValidHybridJWT(token);
|
||||
|
||||
if (!isValidJWT) {
|
||||
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return new Response(JSON.stringify({ msg: e.toString() }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url)
|
||||
const { pathname } = url
|
||||
const path_parts = pathname.split('/')
|
||||
const service_name = path_parts[1]
|
||||
|
||||
if (!service_name || service_name === '') {
|
||||
const error = { msg: 'missing function name in request' }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${service_name}`
|
||||
console.error(`serving the request with ${servicePath}`)
|
||||
|
||||
const memoryLimitMb = 150
|
||||
const workerTimeoutMs = 1 * 60 * 1000
|
||||
const noModuleCache = false
|
||||
const importMapPath = null
|
||||
const envVarsObj = Deno.env.toObject()
|
||||
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
|
||||
|
||||
try {
|
||||
const worker = await EdgeRuntime.userWorkers.create({
|
||||
servicePath,
|
||||
memoryLimitMb,
|
||||
workerTimeoutMs,
|
||||
noModuleCache,
|
||||
importMapPath,
|
||||
envVars,
|
||||
})
|
||||
return await worker.fetch(req)
|
||||
} catch (e) {
|
||||
const error = { msg: e.toString() }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
268
volumes/logs/vector.yml
Normal file
268
volumes/logs/vector.yml
Normal file
@@ -0,0 +1,268 @@
|
||||
api:
|
||||
enabled: true
|
||||
address: 0.0.0.0:9001
|
||||
|
||||
sources:
|
||||
docker_host:
|
||||
type: docker_logs
|
||||
exclude_containers:
|
||||
- supabase-vector
|
||||
|
||||
transforms:
|
||||
project_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- docker_host
|
||||
source: |-
|
||||
.project = "default"
|
||||
.event_message = del(.message)
|
||||
.appname = del(.container_name)
|
||||
del(.container_created_at)
|
||||
del(.container_id)
|
||||
del(.source_type)
|
||||
del(.stream)
|
||||
del(.label)
|
||||
del(.image)
|
||||
del(.host)
|
||||
del(.stream)
|
||||
router:
|
||||
type: route
|
||||
inputs:
|
||||
- project_logs
|
||||
route:
|
||||
kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"'
|
||||
auth: '.appname == "supabase-auth"'
|
||||
rest: '.appname == "supabase-rest"'
|
||||
realtime: '.appname == "realtime-dev.supabase-realtime"'
|
||||
storage: '.appname == "supabase-storage"'
|
||||
functions: '.appname == "supabase-edge-functions"'
|
||||
db: '.appname == "supabase-db"'
|
||||
# Ignores non nginx errors since they are related with kong booting up
|
||||
kong_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.kong
|
||||
source: |-
|
||||
req, err = parse_nginx_log(.event_message, "combined")
|
||||
if err == null {
|
||||
.timestamp = req.timestamp
|
||||
.metadata.request.headers.referer = req.referer
|
||||
.metadata.request.headers.user_agent = req.agent
|
||||
.metadata.request.headers.cf_connecting_ip = req.client
|
||||
.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:
|
||||
- router.kong
|
||||
source: |-
|
||||
.metadata.request.method = "GET"
|
||||
.metadata.response.status_code = 200
|
||||
parsed, err = parse_nginx_log(.event_message, "error")
|
||||
if err == null {
|
||||
.timestamp = parsed.timestamp
|
||||
.severity = parsed.severity
|
||||
.metadata.request.host = parsed.host
|
||||
.metadata.request.headers.cf_connecting_ip = parsed.client
|
||||
url, err = split(parsed.request, " ")
|
||||
if err == null {
|
||||
.metadata.request.method = url[0]
|
||||
.metadata.request.path = url[1]
|
||||
.metadata.request.protocol = url[2]
|
||||
}
|
||||
}
|
||||
if err != null {
|
||||
abort
|
||||
}
|
||||
# Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
|
||||
auth_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.auth
|
||||
source: |-
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata = merge!(.metadata, parsed)
|
||||
}
|
||||
# PostgREST logs are structured so we separate timestamp from message using regex
|
||||
rest_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.rest
|
||||
source: |-
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.timestamp = parse_timestamp!(value: parsed.time,format: "%d/%b/%Y:%H:%M:%S %z")
|
||||
.metadata.host = .project
|
||||
}
|
||||
# Filter out healthcheck logs from Realtime
|
||||
realtime_logs_filtered:
|
||||
type: filter
|
||||
inputs:
|
||||
- router.realtime
|
||||
condition: '!contains(string!(.event_message), "/health")'
|
||||
# Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)
|
||||
realtime_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- realtime_logs_filtered
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.external_id = .metadata.project
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>\d+:\d+:\d+\.\d+) \[(?P<level>\w+)\] (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
}
|
||||
# Function logs are unstructured messages on stderr
|
||||
functions_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.functions
|
||||
source: |-
|
||||
.metadata.project_ref = del(.project)
|
||||
# Storage logs may contain json objects so we parse them for completeness
|
||||
storage_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.storage
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.tenantId = .metadata.project
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata.context[0].host = parsed.hostname
|
||||
.metadata.context[0].pid = parsed.pid
|
||||
}
|
||||
# Postgres logs some messages to stderr which we map to warning severity level
|
||||
db_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.db
|
||||
source: |-
|
||||
.metadata.host = "db-default"
|
||||
.metadata.parsed.timestamp = .timestamp
|
||||
|
||||
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
|
||||
|
||||
if err != null || parsed == null {
|
||||
.metadata.parsed.error_severity = "info"
|
||||
}
|
||||
if parsed.level != null {
|
||||
.metadata.parsed.error_severity = parsed.level
|
||||
}
|
||||
if .metadata.parsed.error_severity == "info" {
|
||||
.metadata.parsed.error_severity = "log"
|
||||
}
|
||||
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
|
||||
|
||||
sinks:
|
||||
logflare_auth:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- auth_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=gotrue.logs.prod'
|
||||
logflare_realtime:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- realtime_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=realtime.logs.prod'
|
||||
logflare_rest:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- rest_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=postgREST.logs.prod'
|
||||
logflare_db:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- db_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
# Route directly to analytics like other sinks. Retry handles the startup
|
||||
# race where analytics may not be ready yet when early DB logs arrive.
|
||||
uri: 'http://analytics:4000/api/logs?source_name=postgres.logs'
|
||||
logflare_functions:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- functions_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=deno-relay-logs'
|
||||
logflare_storage:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- storage_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=storage.logs.prod.2'
|
||||
logflare_kong:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- kong_logs
|
||||
- kong_err
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 30
|
||||
retry_initial_backoff_secs: 1
|
||||
headers:
|
||||
x-api-key: ${LOGFLARE_PUBLIC_ACCESS_TOKEN?LOGFLARE_PUBLIC_ACCESS_TOKEN is required}
|
||||
uri: 'http://analytics:4000/api/logs?source_name=cloudflare.logs.prod'
|
||||
30
volumes/pooler/pooler.exs
Normal file
30
volumes/pooler/pooler.exs
Normal file
@@ -0,0 +1,30 @@
|
||||
{:ok, _} = Application.ensure_all_started(:supavisor)
|
||||
|
||||
{:ok, version} =
|
||||
case Supavisor.Repo.query!("select version()") do
|
||||
%{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
params = %{
|
||||
"external_id" => System.get_env("POOLER_TENANT_ID"),
|
||||
"db_host" => System.get_env("POSTGRES_HOST") || "db",
|
||||
"db_port" => System.get_env("POSTGRES_PORT"),
|
||||
"db_database" => System.get_env("POSTGRES_DB"),
|
||||
"require_user" => false,
|
||||
"auth_query" => "SELECT * FROM pgbouncer.get_auth($1)",
|
||||
"default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"),
|
||||
"default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||
"default_parameter_status" => %{"server_version" => version},
|
||||
"users" => [%{
|
||||
"db_user" => "pgbouncer",
|
||||
"db_password" => System.get_env("POSTGRES_PASSWORD"),
|
||||
"mode_type" => System.get_env("POOLER_POOL_MODE"),
|
||||
"pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"),
|
||||
"is_manager" => true
|
||||
}]
|
||||
}
|
||||
|
||||
if !Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do
|
||||
{:ok, _} = Supavisor.Tenants.create_tenant(params)
|
||||
end
|
||||
18
volumes/proxy/caddy/Caddyfile
Normal file
18
volumes/proxy/caddy/Caddyfile
Normal file
@@ -0,0 +1,18 @@
|
||||
{$PROXY_DOMAIN} {
|
||||
@supabase_api path /auth/v1/* /rest/v1/* /graphql/v1 /realtime/v1/* /storage/v1/* /functions/v1/* /mcp /sso/*
|
||||
|
||||
handle @supabase_api {
|
||||
reverse_proxy kong:8000
|
||||
}
|
||||
|
||||
handle {
|
||||
basic_auth {
|
||||
# PROXY_AUTH_PASSWORD is overwritten with the bcrypt on startup
|
||||
{$PROXY_AUTH_USERNAME} {$PROXY_AUTH_PASSWORD}
|
||||
}
|
||||
|
||||
reverse_proxy studio:3000
|
||||
}
|
||||
|
||||
header -server
|
||||
}
|
||||
95
volumes/proxy/nginx/supabase-nginx.conf.tpl
Normal file
95
volumes/proxy/nginx/supabase-nginx.conf.tpl
Normal file
@@ -0,0 +1,95 @@
|
||||
upstream kong_upstream {
|
||||
server kong:8000;
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
|
||||
server_name ${PROXY_DOMAIN};
|
||||
server_tokens off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/${PROXY_DOMAIN}/privkey.pem;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/chain.pem;
|
||||
|
||||
ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
|
||||
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# Prevent 502 errors from large Supabase auth cookies
|
||||
large_client_header_buffers 4 16k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
|
||||
location / {
|
||||
auth_basic "supabase";
|
||||
auth_basic_user_file /etc/nginx/user_conf.d/dashboard-passwd;
|
||||
|
||||
proxy_pass http://studio:3000;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /rest {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /graphql {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /realtime/v1/ {
|
||||
proxy_pass http://kong_upstream;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
location /storage/v1/ {
|
||||
proxy_pass http://kong_upstream;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
chunked_transfer_encoding off;
|
||||
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
location /functions {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /mcp {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
|
||||
location /sso {
|
||||
proxy_pass http://kong_upstream;
|
||||
}
|
||||
}
|
||||
0
volumes/snippets/.gitkeep
Normal file
0
volumes/snippets/.gitkeep
Normal file
16
volumes/snippets/Untitled query 224.sql
Normal file
16
volumes/snippets/Untitled query 224.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE OR REPLACE FUNCTION public.check_email_exists(p_email text)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
SET search_path = auth
|
||||
AS $$
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM auth.users
|
||||
WHERE email = p_email
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO anon;
|
||||
GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO authenticated;
|
||||
Reference in New Issue
Block a user