Supabase on Kubernetes - Complete Deployment Guide¶
Last Updated: 2025-11-10 Version: v1.0 Tested On: k3s v1.28+, Debian 12
This guide provides complete step-by-step instructions for deploying Supabase on your k3s cluster from scratch.
Prerequisites¶
Before starting, ensure you have completed:
✅ Phase 1: k3s Cluster Installation - 3-node cluster running ✅ Phase 2: kubectl Configuration - kubectl access configured ✅ Phase 3: Core Infrastructure - Longhorn storage + MetalLB LoadBalancer installed
Verify Prerequisites:
# Check cluster is running
kubectl get nodes
# Should show: k3s-master, k3s-worker-1, k3s-worker-2 all Ready
# Check Longhorn is installed
kubectl get sc
# Should show: longhorn (default)
# Check MetalLB is installed
kubectl get pods -n metallb-system
# Should show: controller and speaker pods Running
# Check available IPs for LoadBalancer
# You need 2 available IPs from your MetalLB pool
# Example: 10.89.97.214 and 10.89.97.215
Required Tools:
- kubectl configured and working
- openssl for generating secrets
- Text editor (vim, nano, etc.)
- Optional: Docker for JWT token generation
Architecture Overview¶
We're deploying these components:
PostgreSQL (StatefulSet)
↑
├── GoTrue (Auth) - Deployment
├── PostgREST (REST API) - Deployment
├── Storage (Files) - Deployment
└── postgres-meta (DB Admin) - Deployment
Kong API Gateway (LoadBalancer) ← Entry point for all API requests
Studio Dashboard (LoadBalancer) ← Web UI for management
External Access:
- Kong API: http://<METALLB_IP_1>:8000
- Studio Dashboard: http://<METALLB_IP_2>:3000
Phase 1: Prepare Secrets and Configuration¶
Step 1.1: Generate Secure Passwords and Secrets¶
# Create working directory
mkdir -p ~/supabase-setup
cd ~/supabase-setup
# Generate PostgreSQL password (32 characters)
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d "=+/" | cut -c1-32)
echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> supabase-secrets.env
# Generate JWT Secret (must be 32+ characters)
JWT_SECRET=$(openssl rand -base64 48 | tr -d "=+/" | cut -c1-64)
echo "JWT_SECRET=$JWT_SECRET" >> supabase-secrets.env
# Display generated secrets
cat supabase-secrets.env
IMPORTANT: Save this file somewhere secure! You'll need these values.
Step 1.2: Generate JWT Tokens¶
Supabase requires two JWT tokens: ANON_KEY (public) and SERVICE_ROLE_KEY (admin).
Option A: Using jwt.io (Recommended for beginners)
- Go to https://jwt.io
- Select algorithm: HS256
- Set payload for ANON_KEY:
- In "VERIFY SIGNATURE", paste your
JWT_SECRET(from step 1.1) - Copy the encoded token (long string at top-left)
-
Save as ANON_KEY
-
Repeat for SERVICE_ROLE_KEY with payload:
- Copy encoded token and save as SERVICE_ROLE_KEY
Option B: Using Docker (Advanced)
# Generate ANON_KEY
docker run --rm supabase/gotrue:v2.132.3 \
jwt generate anon \
--secret "$JWT_SECRET" \
--expiry 10y
# Generate SERVICE_ROLE_KEY
docker run --rm supabase/gotrue:v2.132.3 \
jwt generate service_role \
--secret "$JWT_SECRET" \
--expiry 10y
Save the tokens:
# Add to your secrets file
echo "ANON_KEY=<paste-anon-key-here>" >> supabase-secrets.env
echo "SERVICE_ROLE_KEY=<paste-service-role-key-here>" >> supabase-secrets.env
Step 1.3: Choose LoadBalancer IPs¶
Decide which IPs from your MetalLB pool to use:
# Example IPs (adjust to your network):
KONG_IP="10.89.97.214"
STUDIO_IP="10.89.97.215"
echo "KONG_IP=$KONG_IP" >> supabase-secrets.env
echo "STUDIO_IP=$STUDIO_IP" >> supabase-secrets.env
Verify these IPs are available in your MetalLB IP pool configuration.
Phase 2: Create Kubernetes Manifests¶
Step 2.1: Create Namespace¶
Step 2.2: Create Secrets¶
Load your generated secrets:
# Source the secrets file
source ~/supabase-setup/supabase-secrets.env
# Create Kubernetes secret
kubectl create secret generic supabase-secrets \
--namespace=supabase \
--from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
--from-literal=JWT_SECRET="$JWT_SECRET" \
--from-literal=ANON_KEY="$ANON_KEY" \
--from-literal=SERVICE_ROLE_KEY="$SERVICE_ROLE_KEY" \
--from-literal=DATABASE_URL="postgres://postgres:$POSTGRES_PASSWORD@postgres.supabase.svc.cluster.local:5432/postgres?search_path=auth" \
--from-literal=DB_URL="postgres://postgres:$POSTGRES_PASSWORD@postgres.supabase.svc.cluster.local:5432/postgres"
# Verify secret created
kubectl get secret -n supabase supabase-secrets
Step 2.3: Create ConfigMap¶
Create configuration for all services:
cat > /tmp/supabase-configmap.yaml << 'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: supabase-config
namespace: supabase
data:
# API URLs - IMPORTANT: Use LoadBalancer IPs, not internal DNS
API_EXTERNAL_URL: "http://KONG_IP_PLACEHOLDER:8000"
SUPABASE_PUBLIC_URL: "http://KONG_IP_PLACEHOLDER:8000"
SUPABASE_URL: "http://KONG_IP_PLACEHOLDER:8000"
# PostgreSQL Configuration
POSTGRES_HOST: "postgres.supabase.svc.cluster.local"
POSTGRES_PORT: "5432"
POSTGRES_DB: "postgres"
POSTGRES_USER: "postgres"
# PostgREST Configuration
PGRST_DB_SCHEMA: "home_portal,money_tracker,public,storage,graphql_public"
PGRST_DB_ANON_ROLE: "anon"
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_EXP: "3600"
PGRST_APP_SETTINGS_JWT_SECRET: "placeholder"
# GoTrue Configuration
GOTRUE_SITE_URL: "http://localhost:3000"
GOTRUE_URI_ALLOW_LIST: "*"
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "true"
GOTRUE_JWT_AUD: "authenticated"
GOTRUE_JWT_DEFAULT_GROUP_NAME: "authenticated"
GOTRUE_JWT_ADMIN_ROLES: "service_role"
GOTRUE_JWT_EXP: "3600"
# Storage Configuration
STORAGE_BACKEND: "file"
FILE_SIZE_LIMIT: "52428800"
STORAGE_S3_REGION: "us-east-1"
# Service Ports
KONG_HTTP_PORT: "8000"
KONG_HTTPS_PORT: "8443"
STUDIO_PORT: "3000"
REALTIME_PORT: "4000"
REALTIME_DB_URL: "placeholder"
EOF
# Replace placeholder with actual Kong IP
sed -i "s/KONG_IP_PLACEHOLDER/$KONG_IP/g" /tmp/supabase-configmap.yaml
# Apply ConfigMap
kubectl apply -f /tmp/supabase-configmap.yaml
# Verify
kubectl get configmap -n supabase supabase-config
Phase 3: Deploy PostgreSQL¶
PostgreSQL must be deployed first as all other services depend on it.
Step 3.1: Create PostgreSQL StatefulSet¶
cat > /tmp/postgres.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: supabase
labels:
app: postgres
spec:
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgresql
clusterIP: None
selector:
app: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: supabase
labels:
app: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
securityContext:
fsGroup: 102
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: fix-permissions
image: busybox:latest
command:
- sh
- -c
- |
rm -rf /var/lib/postgresql/lost+found
chown -R 101:102 /var/lib/postgresql
chmod 755 /var/lib/postgresql
mkdir -p /run-postgresql
chown 101:102 /run-postgresql
chmod 775 /run-postgresql
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql
- name: postgres-run
mountPath: /run-postgresql
containers:
- name: postgres
image: supabase/postgres:15.1.1.78
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
name: postgresql
protocol: TCP
env:
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_USER
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_DB
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: supabase-secrets
key: POSTGRES_PASSWORD
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql
- name: postgres-run
mountPath: /var/run/postgresql
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
livenessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U postgres
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- /bin/sh
- -c
- pg_isready -U postgres
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
volumes:
- name: postgres-run
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn
resources:
requests:
storage: 20Gi
EOF
# Apply PostgreSQL deployment
kubectl apply -f /tmp/postgres.yaml
# Watch PostgreSQL pod start
kubectl get pods -n supabase -w
# Press Ctrl+C when postgres-0 shows Running
Expected Output:
Step 3.2: Initialize Database Schemas¶
Once PostgreSQL is running, create the required schemas:
# Wait for PostgreSQL to be fully ready
sleep 10
# Create auth schema (required by GoTrue)
kubectl exec -n supabase postgres-0 -- psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS auth;"
# Create storage schema (required by Storage API)
kubectl exec -n supabase postgres-0 -- psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS storage;"
# Create application schemas
kubectl exec -n supabase postgres-0 -- psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS home_portal;"
kubectl exec -n supabase postgres-0 -- psql -U postgres -c "CREATE SCHEMA IF NOT EXISTS money_tracker;"
# Grant permissions
kubectl exec -n supabase postgres-0 -- psql -U postgres << 'EOF'
GRANT USAGE ON SCHEMA auth TO postgres, authenticated, service_role, anon;
GRANT ALL ON SCHEMA auth TO postgres;
GRANT USAGE ON SCHEMA storage TO postgres, authenticated, service_role, anon;
GRANT ALL ON SCHEMA storage TO postgres;
GRANT USAGE ON SCHEMA home_portal TO postgres, authenticated, service_role, anon;
GRANT ALL ON SCHEMA home_portal TO postgres;
GRANT USAGE ON SCHEMA money_tracker TO postgres, authenticated, service_role, anon;
GRANT ALL ON SCHEMA money_tracker TO postgres;
EOF
# Verify schemas created
kubectl exec -n supabase postgres-0 -- psql -U postgres -c "\dn"
You should see: auth, storage, home_portal, money_tracker, public
Phase 4: Deploy Core Services¶
Now deploy the Supabase services in order.
Step 4.1: Deploy GoTrue (Authentication)¶
GoTrue manages user authentication and runs migrations to set up the auth schema.
# Create GoTrue deployment manifest
cat > /tmp/gotrue.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: gotrue
namespace: supabase
labels:
app: gotrue
spec:
type: ClusterIP
ports:
- port: 9999
targetPort: 9999
protocol: TCP
name: http
selector:
app: gotrue
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gotrue
namespace: supabase
labels:
app: gotrue
spec:
replicas: 1
selector:
matchLabels:
app: gotrue
template:
metadata:
labels:
app: gotrue
spec:
containers:
- name: gotrue
image: supabase/gotrue:v2.132.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9999
name: http
protocol: TCP
env:
- name: GOTRUE_API_HOST
value: "0.0.0.0"
- name: GOTRUE_API_PORT
value: "9999"
- name: GOTRUE_DB_DRIVER
value: "postgres"
- name: GOTRUE_DB_DATABASE_URL
valueFrom:
secretKeyRef:
name: supabase-secrets
key: DATABASE_URL
- name: GOTRUE_SITE_URL
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_SITE_URL
- name: GOTRUE_URI_ALLOW_LIST
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_URI_ALLOW_LIST
- name: GOTRUE_DISABLE_SIGNUP
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_DISABLE_SIGNUP
- name: GOTRUE_JWT_ADMIN_ROLES
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_JWT_ADMIN_ROLES
- name: GOTRUE_JWT_AUD
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_JWT_AUD
- name: GOTRUE_JWT_DEFAULT_GROUP_NAME
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_JWT_DEFAULT_GROUP_NAME
- name: GOTRUE_JWT_EXP
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_JWT_EXP
- name: GOTRUE_JWT_SECRET
valueFrom:
secretKeyRef:
name: supabase-secrets
key: JWT_SECRET
- name: GOTRUE_EXTERNAL_EMAIL_ENABLED
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_EXTERNAL_EMAIL_ENABLED
- name: GOTRUE_MAILER_AUTOCONFIRM
valueFrom:
configMapKeyRef:
name: supabase-config
key: GOTRUE_MAILER_AUTOCONFIRM
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 9999
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 9999
initialDelaySeconds: 5
periodSeconds: 5
EOF
# Apply GoTrue
kubectl apply -f /tmp/gotrue.yaml
# Watch GoTrue start and run migrations
kubectl logs -n supabase -l app=gotrue -f
# Press Ctrl+C after you see "GoTrue API started"
Verify GoTrue is running:
Step 4.2: Deploy PostgREST (REST API)¶
PostgREST provides the REST API for database access.
cat > /tmp/postgrest.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: rest
namespace: supabase
labels:
app: rest
spec:
type: ClusterIP
ports:
- port: 3000
targetPort: 3000
protocol: TCP
name: http
selector:
app: rest
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: rest
namespace: supabase
labels:
app: rest
spec:
replicas: 1
selector:
matchLabels:
app: rest
template:
metadata:
labels:
app: rest
spec:
containers:
- name: rest
image: postgrest/postgrest:v12.0.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
protocol: TCP
env:
- name: PGRST_DB_URI
valueFrom:
secretKeyRef:
name: supabase-secrets
key: DB_URL
- name: PGRST_DB_SCHEMAS
valueFrom:
configMapKeyRef:
name: supabase-config
key: PGRST_DB_SCHEMA
- name: PGRST_DB_ANON_ROLE
valueFrom:
configMapKeyRef:
name: supabase-config
key: PGRST_DB_ANON_ROLE
- name: PGRST_JWT_SECRET
valueFrom:
secretKeyRef:
name: supabase-secrets
key: JWT_SECRET
- name: PGRST_DB_USE_LEGACY_GUCS
valueFrom:
configMapKeyRef:
name: supabase-config
key: PGRST_DB_USE_LEGACY_GUCS
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
EOF
# Apply PostgREST
kubectl apply -f /tmp/postgrest.yaml
# Verify
kubectl get pods -n supabase -l app=rest
Step 4.3: Deploy Storage API¶
Storage API manages file uploads and storage.
cat > /tmp/storage-api.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: storage
namespace: supabase
labels:
app: storage
spec:
type: ClusterIP
ports:
- port: 5000
targetPort: 5000
protocol: TCP
name: http
selector:
app: storage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: storage
namespace: supabase
labels:
app: storage
spec:
replicas: 1
selector:
matchLabels:
app: storage
template:
metadata:
labels:
app: storage
spec:
containers:
- name: storage
image: supabase/storage-api:v0.43.11
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
name: http
protocol: TCP
env:
- name: ANON_KEY
valueFrom:
secretKeyRef:
name: supabase-secrets
key: ANON_KEY
- name: SERVICE_KEY
valueFrom:
secretKeyRef:
name: supabase-secrets
key: SERVICE_ROLE_KEY
- name: POSTGREST_URL
value: "http://rest.supabase.svc.cluster.local:3000"
- name: PGRST_JWT_SECRET
valueFrom:
secretKeyRef:
name: supabase-secrets
key: JWT_SECRET
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: supabase-secrets
key: DB_URL
- name: FILE_SIZE_LIMIT
valueFrom:
configMapKeyRef:
name: supabase-config
key: FILE_SIZE_LIMIT
- name: STORAGE_BACKEND
valueFrom:
configMapKeyRef:
name: supabase-config
key: STORAGE_BACKEND
- name: FILE_STORAGE_BACKEND_PATH
value: "/var/lib/storage"
- name: TENANT_ID
value: "stub"
- name: REGION
valueFrom:
configMapKeyRef:
name: supabase-config
key: STORAGE_S3_REGION
volumeMounts:
- name: storage-data
mountPath: /var/lib/storage
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: storage-data
emptyDir: {}
EOF
# Apply Storage
kubectl apply -f /tmp/storage-api.yaml
# Verify
kubectl get pods -n supabase -l app=storage
Step 4.4: Deploy postgres-meta (Database Admin)¶
postgres-meta provides database introspection for Studio.
cat > /tmp/postgres-meta.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: postgres-meta
namespace: supabase
labels:
app: postgres-meta
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: postgres-meta
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-meta
namespace: supabase
labels:
app: postgres-meta
spec:
replicas: 1
selector:
matchLabels:
app: postgres-meta
template:
metadata:
labels:
app: postgres-meta
spec:
containers:
- name: postgres-meta
image: supabase/postgres-meta:v0.68.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: PG_META_PORT
value: "8080"
- name: PG_META_DB_HOST
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_HOST
- name: PG_META_DB_PORT
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_PORT
- name: PG_META_DB_NAME
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_DB
- name: PG_META_DB_USER
valueFrom:
configMapKeyRef:
name: supabase-config
key: POSTGRES_USER
- name: PG_META_DB_PASSWORD
valueFrom:
secretKeyRef:
name: supabase-secrets
key: POSTGRES_PASSWORD
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
EOF
# Apply postgres-meta
kubectl apply -f /tmp/postgres-meta.yaml
# Verify
kubectl get pods -n supabase -l app=postgres-meta
Phase 5: Deploy Kong API Gateway¶
Kong is the entry point for all API requests. It validates JWT tokens and routes to services.
Step 5.1: Create Kong Configuration¶
IMPORTANT: Kong's consumer credentials must match your JWT tokens exactly.
# Load your tokens
source ~/supabase-setup/supabase-secrets.env
# Create Kong config with your actual tokens
cat > /tmp/kong.yaml << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: kong-config
namespace: supabase
data:
kong.yml: |
_format_version: "2.1"
_transform: true
services:
- name: auth-v1-open
url: http://gotrue:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1-open-callback
url: http://gotrue:9999/callback
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
plugins:
- name: cors
- name: auth-v1-open-authorize
url: http://gotrue:9999/authorize
routes:
- name: auth-v1-open-authorize
strip_path: true
paths:
- /auth/v1/authorize
plugins:
- name: cors
- name: auth-v1
url: http://gotrue:9999
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: graphql-v1
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: true
- name: storage-v1
url: http://storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
- name: pg-meta
url: http://postgres-meta:8080/
routes:
- name: pg-meta-all
strip_path: true
paths:
- /pg/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
consumers:
- username: anon
keyauth_credentials:
- key: $ANON_KEY
- username: service_role
keyauth_credentials:
- key: $SERVICE_ROLE_KEY
plugins:
- name: cors
config:
origins:
- "*"
credentials: true
exposed_headers:
- Content-Range
headers:
- authorization
- content-type
- x-client-info
- apikey
- x-upsert
---
apiVersion: v1
kind: Service
metadata:
name: kong
namespace: supabase
labels:
app: kong
spec:
type: LoadBalancer
loadBalancerIP: $KONG_IP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: http
- port: 8443
targetPort: 8443
protocol: TCP
name: https
selector:
app: kong
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kong
namespace: supabase
labels:
app: kong
spec:
replicas: 1
selector:
matchLabels:
app: kong
template:
metadata:
labels:
app: kong
spec:
containers:
- name: kong
image: kong:2.8.1
imagePullPolicy: IfNotPresent
env:
- name: KONG_DATABASE
value: "off"
- name: KONG_DECLARATIVE_CONFIG
value: /usr/local/kong/declarative/kong.yml
- name: KONG_DNS_ORDER
value: LAST,A,CNAME
- name: KONG_PLUGINS
value: request-transformer,cors,key-auth,acl
- name: KONG_NGINX_PROXY_PROXY_BUFFER_SIZE
value: 160k
- name: KONG_NGINX_PROXY_PROXY_BUFFERS
value: 64 160k
ports:
- containerPort: 8000
name: http
protocol: TCP
- containerPort: 8443
name: https
protocol: TCP
volumeMounts:
- name: kong-config
mountPath: /usr/local/kong/declarative
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: kong-config
configMap:
name: kong-config
EOF
# Apply Kong
kubectl apply -f /tmp/kong.yaml
# Watch Kong start
kubectl get pods -n supabase -l app=kong -w
# Press Ctrl+C when Running
Verify Kong LoadBalancer IP:
Phase 6: Deploy Studio Dashboard¶
Studio is the web UI for managing Supabase.
# Load environment
source ~/supabase-setup/supabase-secrets.env
cat > /tmp/studio.yaml << EOF
apiVersion: v1
kind: Service
metadata:
name: studio
namespace: supabase
labels:
app: studio
spec:
type: LoadBalancer
loadBalancerIP: $STUDIO_IP
ports:
- port: 3000
targetPort: 3000
protocol: TCP
name: http
selector:
app: studio
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: studio
namespace: supabase
labels:
app: studio
spec:
replicas: 1
selector:
matchLabels:
app: studio
template:
metadata:
labels:
app: studio
spec:
containers:
- name: studio
image: supabase/studio:20240729-ce42139
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
protocol: TCP
env:
- name: STUDIO_PG_META_URL
value: "http://postgres-meta.supabase.svc.cluster.local:8080"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: supabase-secrets
key: POSTGRES_PASSWORD
- name: SUPABASE_URL
value: "http://$KONG_IP:8000"
- name: SUPABASE_PUBLIC_URL
value: "http://$KONG_IP:8000"
- name: SUPABASE_ANON_KEY
valueFrom:
secretKeyRef:
name: supabase-secrets
key: ANON_KEY
- name: SUPABASE_SERVICE_KEY
valueFrom:
secretKeyRef:
name: supabase-secrets
key: SERVICE_ROLE_KEY
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
EOF
# Apply Studio
kubectl apply -f /tmp/studio.yaml
# Watch Studio start
kubectl get pods -n supabase -l app=studio -w
# Press Ctrl+C when Running
Verify Studio LoadBalancer IP:
Phase 7: Verification and Testing¶
Step 7.1: Check All Pods Running¶
Expected output (all should be Running):
NAME READY STATUS RESTARTS AGE
postgres-0 1/1 Running 0 15m
gotrue-xxxxxxxxx-xxxxx 1/1 Running 0 10m
rest-xxxxxxxxx-xxxxx 1/1 Running 0 8m
storage-xxxxxxxxx-xxxxx 1/1 Running 0 6m
postgres-meta-xxxxxxxxx-xxxxx 1/1 Running 0 5m
kong-xxxxxxxxx-xxxxx 1/1 Running 0 3m
studio-xxxxxxxxx-xxxxx 1/1 Running 0 2m
Step 7.2: Test Kong API Gateway¶
Step 7.3: Access Studio Dashboard¶
Open your browser and go to:
Example: http://10.89.97.215:3000
You should see the Supabase Studio interface.
Step 7.4: Create First User (Test Authentication)¶
In Studio: 1. Click "Authentication" in left sidebar 2. Click "Users" tab 3. Click "Add user" button 4. Fill in: - Email: test@example.com - Password: TestPassword123! - Auto Confirm User: ✅ (checked) 5. Click "Create user"
If this works: ✅ Your Supabase installation is successful!
If you get 401 Unauthorized: See troubleshooting section below.
Phase 8: Post-Deployment Configuration¶
Step 8.1: Save Manifest Files¶
Save all your manifests for future reference:
# Create manifests directory
mkdir -p /root/tower-fleet/manifests/supabase
# Copy all generated manifests
cp /tmp/supabase-configmap.yaml /root/tower-fleet/manifests/supabase/configmap.yaml
cp /tmp/postgres.yaml /root/tower-fleet/manifests/supabase/postgres.yaml
cp /tmp/gotrue.yaml /root/tower-fleet/manifests/supabase/gotrue.yaml
cp /tmp/postgrest.yaml /root/tower-fleet/manifests/supabase/postgrest.yaml
cp /tmp/storage-api.yaml /root/tower-fleet/manifests/supabase/storage-api.yaml
cp /tmp/postgres-meta.yaml /root/tower-fleet/manifests/supabase/postgres-meta.yaml
cp /tmp/kong.yaml /root/tower-fleet/manifests/supabase/kong.yaml
cp /tmp/studio.yaml /root/tower-fleet/manifests/supabase/studio.yaml
# Create namespace manifest
cat > /root/tower-fleet/manifests/supabase/namespace.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: supabase
EOF
# Create secrets template (WITHOUT actual values - for reference only)
cat > /root/tower-fleet/manifests/supabase/secrets.yaml << 'EOF'
# DO NOT COMMIT THIS FILE TO GIT!
# This is a template - actual secrets are in Kubernetes
apiVersion: v1
kind: Secret
metadata:
name: supabase-secrets
namespace: supabase
type: Opaque
stringData:
POSTGRES_PASSWORD: "<generated-in-step-1.1>"
JWT_SECRET: "<generated-in-step-1.1>"
ANON_KEY: "<generated-in-step-1.2>"
SERVICE_ROLE_KEY: "<generated-in-step-1.2>"
DATABASE_URL: "postgres://postgres:<password>@postgres.supabase.svc.cluster.local:5432/postgres?search_path=auth"
DB_URL: "postgres://postgres:<password>@postgres.supabase.svc.cluster.local:5432/postgres"
EOF
echo "✅ Manifests saved to /root/tower-fleet/manifests/supabase/"
Step 8.2: Update Home Portal Configuration¶
See Home Portal Documentation for integrating apps with Supabase.
Quick summary:
# Get your Supabase credentials
echo "NEXT_PUBLIC_SUPABASE_URL=http://$KONG_IP:8000"
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY"
Use these values in your application's .env.local file.
Troubleshooting¶
Issue: Pods Stuck in Pending¶
Symptom: Pods show STATUS: Pending for more than 2 minutes
Cause: No storage available or no LoadBalancer IP available
Solution:
# Check if Longhorn is running
kubectl get pods -n longhorn-system
# Check if MetalLB is running
kubectl get pods -n metallb-system
# Check pod events
kubectl describe pod -n supabase <pod-name>
Issue: PostgreSQL Won't Start¶
Symptom: postgres-0 shows CrashLoopBackOff or Error
Check logs:
Common causes: - Permission issues → Check initContainer logs - Corrupted data → Delete PVC and recreate
Fix (nuclear option - deletes all data):
kubectl delete pvc -n supabase postgres-data-postgres-0
kubectl delete pod -n supabase postgres-0
# Wait for pod to recreate with fresh volume
Issue: GoTrue Migration Fails¶
Symptom: GoTrue pod logs show migration errors
Known issue: Migration 20221208132122 may fail with UUID/text type error
Solution:
# Manually mark migration as complete
kubectl exec -n supabase postgres-0 -- psql -U postgres -c \
"INSERT INTO auth.schema_migrations (version) VALUES ('20221208132122') ON CONFLICT DO NOTHING;"
# Restart GoTrue
kubectl rollout restart deployment -n supabase gotrue
Issue: 401 Unauthorized Creating Users in Studio¶
Symptom: Studio shows "Invalid authentication credentials" or 401 error
Cause: JWT tokens in Kong ConfigMap don't match supabase-secrets
Solution: See JWT Token Management
Quick check:
# Check if Kong has correct tokens
kubectl exec -n supabase -l app=kong -- cat /usr/local/kong/declarative/kong.yml | grep -A 3 "consumers:"
# Compare with secrets
kubectl get secret -n supabase supabase-secrets -o jsonpath='{.data.SERVICE_ROLE_KEY}' | base64 -d
If different: Kong needs to be updated with correct tokens (see JWT Token Management guide).
Issue: Studio Can't Connect to Services¶
Symptom: Studio loads but shows "Cannot connect to server" errors
Cause: Studio using internal Kubernetes DNS instead of LoadBalancer IP
Solution:
# Verify Studio has correct SUPABASE_URL
kubectl exec -n supabase -l app=studio -- env | grep SUPABASE_URL
# Should show: SUPABASE_URL=http://<KONG_IP>:8000
# NOT: SUPABASE_URL=http://kong.supabase.svc.cluster.local:8000
If wrong: Update Studio deployment with correct LoadBalancer IP and restart.
Summary¶
You now have a fully functional Supabase deployment! 🎉
Access Points:
- API Gateway: http://<KONG_IP>:8000
- Studio Dashboard: http://<STUDIO_IP>:3000
- PostgreSQL: postgres.supabase.svc.cluster.local:5432 (internal only)
Next Steps: 1. Configure applications to use Supabase 2. Set up Row Level Security policies 3. Create database schemas and tables
Architecture Documentation: - Supabase Architecture Overview - Multi-App Setup - JWT Token Management
Additional Resources¶
Questions or Issues?
Check the Troubleshooting Guide or review the Deployment Lessons Learned.