Compare commits

...
Author SHA1 Message Date
Mauricio SiuandClaude Fable 5 17a53fa567 fix(supabase): upgrade database to Postgres 17
Align the template with the current upstream self-hosted stack
(supabase/postgres 17.6.1.136, studio 2026.08.03, kong 3.9.3),
remove the deprecated analytics/vector services and switch
Studio/pg-meta to the postgres role per upstream guidance.

Closes #1067

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 01:09:28 -06:00
3 changed files with 23 additions and 371 deletions
+21 -81
View File
@@ -11,7 +11,7 @@ name: supabase
services:
studio:
image: supabase/studio:2026.06.03-sha-0bca601
image: supabase/studio:2026.08.03-sha-022b374
restart: unless-stopped
healthcheck:
test:
@@ -23,9 +23,6 @@ services:
interval: 5s
retries: 5
start_period: 30s
depends_on:
analytics:
condition: service_healthy
environment:
HOSTNAME: "0.0.0.0"
@@ -35,6 +32,9 @@ services:
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}
@@ -49,14 +49,12 @@ services:
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:-}
LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
# Analytics (Logflare) was removed from the upstream self-hosted stack
ENABLED_FEATURES_LOGS_ALL: "false"
LOGFLARE_URL: http://analytics:4000
NEXT_PUBLIC_ENABLE_LOGS: "true"
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
volumes:
@@ -64,7 +62,7 @@ services:
- ../files/volumes/functions:/app/edge-functions:Z
kong:
image: kong/kong:3.9.1
image: kong/kong:3.9.3
restart: unless-stopped
healthcheck:
test: ["CMD", "kong", "health"]
@@ -138,6 +136,7 @@ services:
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
@@ -163,12 +162,19 @@ services:
depends_on:
db:
condition: service_healthy
healthcheck:
test: [ "CMD", "postgrest", "--ready" ]
interval: 5s
timeout: 5s
retries: 3
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
PGRST_DB_ANON_ROLE: anon
PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_HOST: localhost
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
@@ -292,7 +298,8 @@ services:
PG_META_DB_HOST: ${POSTGRES_HOST}
PG_META_DB_PORT: ${POSTGRES_PORT}
PG_META_DB_NAME: ${POSTGRES_DB}
PG_META_DB_USER: supabase_admin
# See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access
PG_META_DB_USER: postgres
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
@@ -322,43 +329,9 @@ services:
"/home/deno/functions/main"
]
analytics:
image: supabase/logflare:1.36.1
restart: unless-stopped
expose:
- 4000
# First boot: Logflare seeds _analytics in Postgres; allow extra time before marking unhealthy.
healthcheck:
test:
[
"CMD-SHELL",
"curl -sSfL -o /dev/null http://localhost:4000/health"
]
timeout: 10s
interval: 5s
retries: 15
start_period: 90s
depends_on:
db:
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
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
LOGFLARE_SINGLE_TENANT: "true"
LOGFLARE_SUPABASE_MODE: "true"
LOGFLARE_MIN_CLUSTER_SIZE: 1
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
POSTGRES_BACKEND_SCHEMA: _analytics
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
db:
# Major upgrade from Postgres 15 requires a dump/restore for existing installs.
# See: https://github.com/orgs/supabase/discussions/46080
image: supabase/postgres:17.6.1.136
restart: unless-stopped
volumes:
@@ -385,9 +358,6 @@ services:
timeout: 5s
retries: 15
start_period: 60s
depends_on:
vector:
condition: service_healthy
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: ${POSTGRES_PORT}
@@ -407,36 +377,6 @@ services:
"log_min_messages=fatal"
]
vector:
image: timberio/vector:0.53.0-alpine
restart: unless-stopped
volumes:
- ../files/volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z
healthcheck:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://vector:9001/health"
]
timeout: 5s
interval: 5s
retries: 5
start_period: 10s
environment:
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
command:
[
"--config",
"/etc/vector/vector.yml"
]
security_opt:
- "label=disable"
supavisor:
image: supabase/supavisor:2.9.5
restart: unless-stopped
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "supabase",
"name": "SupaBase",
"version": "2026.06.03 / dokploy >= 0.22.5",
"version": "2026.08.03 / dokploy >= 0.22.5",
"description": "The open source Firebase alternative. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications. This require at least version 0.22.5 of dokploy.",
"links": {
"github": "https://github.com/supabase/supabase",
+1 -289
View File
@@ -2,8 +2,6 @@
main_domain = "${domain}"
postgres_password = "${password:32}"
dashboard_password = "${password:32}"
logflare_public_access_token = "${password:32}"
logflare_private_access_token = "${password:32}"
pg_meta_crypto_key = "${password:32}"
s3_protocol_access_key_id = "${password:24}"
s3_protocol_access_key_secret = "${password:48}"
@@ -59,8 +57,6 @@ env = [
'SECRET_KEY_BASE=${secret_key_base}',
'VAULT_ENC_KEY=${vault_enc_key}',
'PG_META_CRYPTO_KEY=${pg_meta_crypto_key}',
'LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}',
'LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}',
'',
'',
'############',
@@ -164,19 +160,7 @@ env = [
'# Functions - Configuration for Functions',
'############',
'# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet.',
'FUNCTIONS_VERIFY_JWT=false',
'',
'',
'############',
'# Logs - Configuration for Logflare',
'############',
'',
'# Docker socket location - this value will differ depending on your OS',
'DOCKER_SOCKET_LOCATION=/var/run/docker.sock',
'',
'# Google Cloud Project details',
'GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID',
'GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER']
'FUNCTIONS_VERIFY_JWT=false']
[[config.mounts]]
filePath = "/volumes/api/kong-entrypoint.sh"
@@ -1007,278 +991,6 @@ Deno.serve(async (req: Request) => {
})
"""
[[config.mounts]]
filePath = "/volumes/logs/vector.yml"
content = """api:
enabled: true
address: 0.0.0.0:9001
sources:
docker_host:
type: docker_logs
transforms:
project_logs:
type: remap
inputs:
- docker_host
source: |-
.project = "default"
.event_message = del(.message)
compose_service, label_err = get(.label, ["com.docker.compose.service"])
if label_err != null || compose_service == null {
abort
}
compose_service = to_string!(compose_service)
if compose_service == "vector" {
abort
}
.appname = "supabase-" + compose_service
del(.container_created_at)
del(.container_id)
del(.source_type)
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 == "supabase-realtime"'
storage: '.appname == "supabase-storage"'
functions: '.appname == "supabase-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
}
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
}
realtime_logs_filtered:
type: filter
inputs:
- router.realtime
condition: '!contains(string!(.event_message), "/health")'
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
}
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}
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'
"""
[[config.mounts]]
filePath = "/volumes/pooler/pooler.exs"
content = """{:ok, _} = Application.ensure_all_started(:supavisor)