Deploy Supabase self-hosted with Docker, Traefik, and Authentik
Run the official self-hosted Supabase stack with Studio protected by Authentik, Kong exposed as the app API, and Postgres kept private.
Supabase gives you a full backend platform: PostgreSQL, Auth, REST, Realtime, Storage, Edge Functions, Studio, logs, image proxying, and a connection pooler. The clean homelab pattern is to expose only the pieces that need to be reached:
- Studio is an operator dashboard, so protect it with Authentik.
- Kong is the public API gateway for apps, so expose it with HTTPS but secure data with keys, JWTs, RLS, and policies.
- PostgreSQL, pooler, storage internals, Realtime internals, metadata, analytics, and Vector stay on private Docker networks or localhost binds.
All hostnames, IPs, passwords, JWT secrets, and API keys below are placeholders. Replace supabase.example.com, supabase-api.example.com, and passwords with your own values.
Never publish real Supabase
anon,service_role, JWT, Postgres, SMTP, or dashboard credentials in public documentation. Keep them in.env, restrict file permissions, and rotate anything that leaks.
What this service does
Route or access pattern:
1
2
3
4
https://supabase.example.com -> Supabase Studio dashboard, protected by Authentik
https://supabase-api.example.com -> Supabase Kong API gateway for app clients
127.0.0.1:5432 -> optional local Postgres/pooler access only
127.0.0.1:6543 -> optional local pooler access only
Main components:
1
Supabase Studio, Kong, GoTrue Auth, PostgREST, Realtime, Storage API, Edge Runtime, Postgres, Supavisor, Postgres Meta, Logflare, Vector, imgproxy, Traefik, Authentik.
Network model:
1
2
3
4
5
Browser / App
-> Traefik HTTPS
-> Studio or Kong
-> internal Supabase services
-> PostgreSQL
The important part is that applications talk to the API gateway, not directly to Postgres.
Related documentation
Read these first if this is part of the same homelab platform:
- Deploy Traefik Reverse Proxy with Docker and Cloudflare
- Deploy Authentik SSO with Docker and Traefik
- Deploy Cloudflare Companion for Traefik DNS Automation
- Deploy PocketBase Backend with Docker and Traefik
- Deploy WatchParty with Docker, Traefik, Firebase, and Headscale
Folder layout
Use one dedicated directory for the Supabase stack.
1
2
3
4
5
6
7
8
9
10
11
12
13
/home/ubuntu/supabase/
├── docker-compose.yml # official Supabase compose file
├── docker-compose.override.yml # local Traefik/Auth routing
├── .env # private secrets and host settings
├── volumes/
│ ├── api/
│ │ ├── kong.yml
│ │ └── kong-entrypoint.sh
│ ├── db/
│ ├── functions/
│ ├── logs/
│ └── storage/
└── backups/
Keep .env private:
1
2
3
cd /home/ubuntu/supabase
chmod 600 .env
mkdir -p backups
Get the official Supabase Docker stack
Edit files under /home/ubuntu/supabase.
1
2
3
4
5
6
mkdir -p /home/ubuntu/supabase
cd /home/ubuntu/supabase
git clone --depth 1 https://github.com/supabase/supabase.git /tmp/supabase-source
cp -a /tmp/supabase-source/docker/. /home/ubuntu/supabase/
cp .env.example .env
Then review the upstream files before editing:
1
2
ls -la
ls -la volumes/api volumes/db volumes/functions
Environment file
Edit /home/ubuntu/supabase/.env.
This is not a complete upstream .env; it shows the values you must deliberately set for a real deployment.
############
# Hostnames
############
SUPABASE_PUBLIC_URL=https://supabase-api.example.com
API_EXTERNAL_URL=https://supabase-api.example.com
SITE_URL=https://app.example.com
ADDITIONAL_REDIRECT_URLS=https://app.example.com,https://supabase.example.com
########################
# Studio default labels
########################
STUDIO_DEFAULT_ORGANIZATION=Homelab
STUDIO_DEFAULT_PROJECT=Default Project
###################
# Dashboard access
###################
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=<strong-dashboard-password>
###################
# Postgres settings
###################
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432
POSTGRES_PASSWORD=<strong-postgres-password>
###################
# Local port binds
###################
KONG_HTTP_PORT=127.0.0.1:8004
KONG_HTTPS_PORT=127.0.0.1:8444
POSTGRES_PORT=5432
POOLER_PROXY_PORT_TRANSACTION=6543
###################
# JWT / API keys
###################
JWT_SECRET=<long-random-jwt-secret>
ANON_KEY=<generated-anon-key>
SERVICE_ROLE_KEY=<generated-service-role-key>
###################
# Auth behavior
###################
DISABLE_SIGNUP=false
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
###################
# SMTP
###################
[email protected]
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=smtp-user
SMTP_PASS=<smtp-password>
SMTP_SENDER_NAME=Supabase
Generate real secrets locally; do not reuse the placeholders:
1
2
openssl rand -base64 48
openssl rand -hex 32
For the Supabase JWT keys, follow the official Supabase self-hosting docs or use the Supabase CLI/key generator and place the generated values in .env.
Docker Compose stack
Edit /home/ubuntu/supabase/docker-compose.yml.
The official Supabase Docker Compose file is large because Supabase is not one container. It is a coordinated platform. If you copied the upstream file from supabase/docker, keep it as your base. The example below shows a readable production-style Compose layout with the important services and wiring kept explicit.
Prefer starting from the official Supabase
docker-compose.ymland then applying the local override shown later. Use this full example to understand what each service does and what must stay private.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
name: supabase
services:
studio:
image: supabase/studio:2026.04.27-sha-5f60601
container_name: supabase-studio
restart: unless-stopped
depends_on:
analytics:
condition: service_healthy
environment:
HOSTNAME: 0.0.0.0
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
LOGFLARE_URL: http://analytics:4000
NEXT_PUBLIC_ENABLE_LOGS: "true"
NEXT_ANALYTICS_BACKEND_PROVIDER: postgres
volumes:
- ./volumes/snippets:/app/snippets:Z
- ./volumes/functions:/app/edge-functions:Z
networks:
- default
kong:
image: kong/kong:3.9.1
container_name: supabase-kong
restart: unless-stopped
depends_on:
analytics:
condition: service_healthy
ports:
- ${KONG_HTTP_PORT}:8000/tcp
- ${KONG_HTTPS_PORT}:8443/tcp
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
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
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
volumes:
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
entrypoint: /home/kong/kong-entrypoint.sh
networks:
default:
aliases:
- api-gw
auth:
image: supabase/gotrue:v2.186.0
container_name: supabase-auth
restart: unless-stopped
depends_on:
db:
condition: service_healthy
analytics:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
GOTRUE_SITE_URL: ${SITE_URL}
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
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_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
networks:
- default
rest:
image: postgrest/postgrest:v14.8
container_name: supabase-rest
restart: unless-stopped
depends_on:
db:
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_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
command: ["postgrest"]
networks:
- default
realtime:
image: supabase/realtime:v2.76.5
container_name: realtime-dev.supabase-realtime
restart: unless-stopped
depends_on:
db:
condition: service_healthy
analytics:
condition: service_healthy
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: ${REALTIME_DB_ENC_KEY}
API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: "true"
RUN_JANITOR: "true"
networks:
- default
storage:
image: supabase/storage-api:v1.48.26
container_name: supabase-storage
restart: unless-stopped
depends_on:
db:
condition: service_healthy
rest:
condition: service_started
imgproxy:
condition: service_healthy
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
FILE_SIZE_LIMIT: ${STORAGE_FILE_SIZE_LIMIT}
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
volumes:
- ./volumes/storage:/var/lib/storage:z
networks:
- default
imgproxy:
image: darthsim/imgproxy:v3.30.1
container_name: supabase-imgproxy
restart: unless-stopped
environment:
IMGPROXY_BIND: :5001
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
volumes:
- ./volumes/storage:/var/lib/storage:z
networks:
- default
meta:
image: supabase/postgres-meta:v0.96.3
container_name: supabase-meta
restart: unless-stopped
depends_on:
db:
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}
networks:
- default
functions:
image: supabase/edge-runtime:v1.71.2
container_name: supabase-edge-functions
restart: unless-stopped
depends_on:
analytics:
condition: service_healthy
environment:
JWT_SECRET: ${JWT_SECRET}
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
VERIFY_JWT: "false"
volumes:
- ./volumes/functions:/home/deno/functions:Z
command:
- start
- --main-service
- /home/deno/functions/main
networks:
- default
analytics:
image: supabase/logflare:1.36.1
container_name: supabase-analytics
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
LOGFLARE_NODE_HOST: 127.0.0.1
DB_USERNAME: supabase_admin
DB_DATABASE: ${POSTGRES_DB}
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"
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
POSTGRES_BACKEND_SCHEMA: _analytics
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
networks:
- default
vector:
image: timberio/vector:0.53.0-alpine
container_name: supabase-vector
restart: unless-stopped
volumes:
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
- ${DOCKER_SOCKET_LOCATION:-/var/run/docker.sock}:/var/run/docker.sock:ro,z
command: ["--config", "/etc/vector/vector.yml"]
networks:
- default
db:
image: supabase/postgres:15.8.1.085
container_name: supabase-db
restart: unless-stopped
volumes:
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
- ./volumes/db/data:/var/lib/postgresql/data:Z
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}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost"]
interval: 5s
timeout: 5s
retries: 10
networks:
- default
supavisor:
image: supabase/supavisor:2.7.4
container_name: supabase-pooler
restart: unless-stopped
depends_on:
db:
condition: service_healthy
analytics:
condition: service_healthy
ports:
- 127.0.0.1:5432:5432
- 127.0.0.1:6543:6543
environment:
PORT: 4000
POSTGRES_PORT: ${POSTGRES_PORT}
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
networks:
- default
networks:
default:
name: supabase_default
The upstream file may contain more environment variables than this readable example. Keep upstream additions when upgrading.
Kong declarative config
Edit /home/ubuntu/supabase/volumes/api/kong.yml.
Kong maps public API paths to internal Supabase services. The upstream file includes all routes, but the core idea looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
_format_version: "2.1"
_transform: true
consumers:
- username: anon
keyauth_credentials:
- key: ${SUPABASE_ANON_KEY}
- username: service_role
keyauth_credentials:
- key: ${SUPABASE_SERVICE_KEY}
services:
- name: auth-v1-open
url: http://auth:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1
url: http://auth:9999/
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
- name: realtime-v1
url: http://realtime:4000/socket/
routes:
- name: realtime-v1-all
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
- name: key-auth
- name: storage-v1
url: http://storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
- name: key-auth
- name: functions-v1
url: http://functions:9000/
routes:
- name: functions-v1-all
strip_path: true
paths:
- /functions/v1/
plugins:
- name: cors
Do not hand-roll Kong unless you need to. Use the upstream volumes/api/kong.yml as the source of truth, then verify it has the routes your SDK expects.
Compose override for Traefik and Authentik
Edit /home/ubuntu/supabase/docker-compose.override.yml.
This override leaves the official compose mostly untouched and adds two public routes:
- Studio route: protected by Authentik.
- Kong API route: public app API route, not behind Authentik.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
services:
studio:
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.middlewares.supabase-studio-https.redirectscheme.scheme=https"
- "traefik.http.routers.supabase-studio.entrypoints=http"
- "traefik.http.routers.supabase-studio.rule=Host(`supabase.example.com`)"
- "traefik.http.routers.supabase-studio.middlewares=supabase-studio-https"
- "traefik.http.routers.supabase-studio-secure.entrypoints=https"
- "traefik.http.routers.supabase-studio-secure.rule=Host(`supabase.example.com`)"
- "traefik.http.routers.supabase-studio-secure.tls=true"
- "traefik.http.routers.supabase-studio-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.supabase-studio-secure.middlewares=authentik@docker"
- "traefik.http.routers.supabase-studio-secure.service=supabase-studio"
- "traefik.http.services.supabase-studio.loadbalancer.server.port=3000"
kong:
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.middlewares.supabase-api-https.redirectscheme.scheme=https"
- "traefik.http.routers.supabase-api.entrypoints=http"
- "traefik.http.routers.supabase-api.rule=Host(`supabase-api.example.com`)"
- "traefik.http.routers.supabase-api.middlewares=supabase-api-https"
- "traefik.http.routers.supabase-api-secure.entrypoints=https"
- "traefik.http.routers.supabase-api-secure.rule=Host(`supabase-api.example.com`)"
- "traefik.http.routers.supabase-api-secure.tls=true"
- "traefik.http.routers.supabase-api-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.supabase-api-secure.service=supabase-api"
- "traefik.http.services.supabase-api.loadbalancer.server.port=8000"
networks:
proxy:
external: true
Do not put authentik@docker on the Kong API route unless every client is a browser user. Mobile apps, server apps, and frontend SDKs need direct API access.
Authentik outpost route for Studio
If you use embedded outpost routing in the Authentik compose file, edit /home/ubuntu/authentik/docker-compose.yml and add an outpost route for the Studio hostname.
Add this under the authentik-server Traefik labels:
1
2
3
4
5
6
- "traefik.http.routers.authentik-supabase-outpost.entrypoints=https"
- "traefik.http.routers.authentik-supabase-outpost.rule=Host(`supabase.example.com`) && PathPrefix(`/outpost.goauthentik.io/`)"
- "traefik.http.routers.authentik-supabase-outpost.priority=100"
- "traefik.http.routers.authentik-supabase-outpost.tls=true"
- "traefik.http.routers.authentik-supabase-outpost.tls.certresolver=cloudflare"
- "traefik.http.routers.authentik-supabase-outpost.service=authentik"
Then restart Authentik:
1
2
cd /home/ubuntu/authentik
docker compose up -d
Start the stack
Run from /home/ubuntu/supabase:
1
2
3
4
5
6
7
8
9
10
11
cd /home/ubuntu/supabase
docker compose \
-f docker-compose.yml \
-f docker-compose.override.yml \
pull
docker compose \
-f docker-compose.yml \
-f docker-compose.override.yml \
up -d
Check the containers:
1
2
3
4
docker compose \
-f docker-compose.yml \
-f docker-compose.override.yml \
ps
Expected services include:
1
2
3
4
5
6
7
8
9
10
11
12
13
supabase-studio
supabase-kong
supabase-auth
supabase-rest
supabase-realtime
supabase-storage
supabase-db
supabase-pooler
supabase-meta
supabase-edge-functions
supabase-analytics
supabase-vector
supabase-imgproxy
Verify Studio
From a browser, open:
1
https://supabase.example.com
Expected result:
1
Authentik login -> Supabase Studio dashboard
From the server, check the container health:
1
docker inspect supabase-studio --format '' | jq
If Studio loads but cannot query the database, check:
1
2
3
docker logs --tail=200 supabase-studio
docker logs --tail=200 supabase-meta
docker logs --tail=200 supabase-db
Verify the API gateway
Check Kong from the server:
1
curl -i http://127.0.0.1:8004/auth/v1/health
Check it through Traefik:
1
curl -i https://supabase-api.example.com/auth/v1/health
You should see a healthy HTTP response from GoTrue/Auth. If the route returns 404, inspect Kong and Traefik labels. If it returns an Authentik login page, you accidentally put Authentik middleware on the app API route.
Create an example application client
In your frontend application, configure the Supabase SDK with the API hostname, not the Studio hostname.
Edit your app .env file, for example /home/ubuntu/my-app/.env:
VITE_SUPABASE_URL=https://supabase-api.example.com
VITE_SUPABASE_ANON_KEY=<anon-key-from-supabase-env>
Example JavaScript client:
1
2
3
4
5
6
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
)
Never put the service_role key in a browser application.
Row-level security baseline
Supabase is only safe when RLS policies match your application model.
Example table migration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
display_name text,
avatar_url text,
created_at timestamptz not null default now()
);
alter table public.profiles enable row level security;
create policy "profiles are readable by authenticated users"
on public.profiles
for select
to authenticated
using (true);
create policy "users can update their own profile"
on public.profiles
for update
to authenticated
using (auth.uid() = id)
with check (auth.uid() = id);
Run migrations from Studio SQL editor or from a controlled migration tool. Back up first if the database has real data.
Backups
Create /home/ubuntu/supabase/backup.sh:
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env bash
set -euo pipefail
cd /home/ubuntu/supabase
mkdir -p backups
STAMP="$(date +%Y%m%d-%H%M%S)"
docker exec supabase-db pg_dumpall -U postgres | gzip > "backups/supabase-${STAMP}.sql.gz"
find backups -type f -name 'supabase-*.sql.gz' -mtime +14 -delete
Make it executable:
1
chmod +x /home/ubuntu/supabase/backup.sh
Add a cron job:
15 3 * * * /home/ubuntu/supabase/backup.sh >> /home/ubuntu/supabase/backups/backup.log 2>&1
Updating Supabase
Before updating:
1
2
cd /home/ubuntu/supabase
./backup.sh
Then update carefully:
1
2
3
4
5
6
7
8
9
10
11
12
git -C /tmp/supabase-source pull --ff-only
# Review upstream changes before replacing local compose files.
docker compose \
-f docker-compose.yml \
-f docker-compose.override.yml \
pull
docker compose \
-f docker-compose.yml \
-f docker-compose.override.yml \
up -d
Do not blindly overwrite .env, volumes/api/kong.yml, or local overrides without reviewing changes.
Troubleshooting
Studio redirects but API does not work
Check that the app uses:
1
https://supabase-api.example.com
not:
1
https://supabase.example.com
Studio and API are different routes.
API is behind Authentik by mistake
Inspect labels:
1
docker inspect supabase-kong --format '{json .Config.Labels}' | jq
The Kong route should not include:
1
authentik@docker
Postgres is exposed publicly
Check published ports:
1
docker ps --format 'table {.Names} {.Ports}' | grep supabase
Postgres and pooler should be localhost-only or internal-only, not 0.0.0.0.
DNS record is missing
If you use Cloudflare Companion, restart it after adding new Traefik hosts:
1
2
cd /home/ubuntu/companion
docker compose restart
Security checklist
- Studio protected by Authentik.
- API route public only through Kong.
- Postgres and pooler not exposed on public interfaces.
- Strong JWT secret and generated API keys.
- SMTP configured for Auth emails.
- RLS enabled on application tables.
service_rolekey never shipped to browsers.- Backups scheduled and restore-tested.
.envpermissions restricted.