How to Install Self-Hosted Supabase on Ubuntu 24.04 VPS: The Open-Source Firebase Alternative
Supabase is the open-source backend-as-a-service (BaaS) that has become the de facto alternative to Firebase for teams who want Postgres-grade power, open standards, and the option to self-host the entire stack. With a single Docker Compose deployment, you get a production-ready Postgres 15 database, an auto-generated REST API, a realtime WebSocket server, an authentication service, S3-compatible object storage, Deno-powered edge functions, and a polished web Studio for managing it all.
This guide walks you through a complete self-hosted Supabase install on Ubuntu 24.04 LTS, from provisioning the VPS to exposing Studio over HTTPS on your own domain. By the end you will have a fully functional Supabase stack that you control end-to-end, with no per-row pricing and no data leaving your server.
Want the speed of a managed platform with the control of self-hosting? Deploy Supabase on a CloudCore Business VPS — our best-value plan for backend services with 8 vCPU, 24 GB RAM and 200 GB NVMe storage.
Table of Contents
What is Self-Hosted Supabase?
Supabase is an open-source developer platform that bundles seven production services behind a single unified API. When you self-host, you run the exact same containers that power Supabase Cloud on your own infrastructure:
- Postgres 15 — the primary database, with all Supabase extensions pre-enabled (
pgcrypto,pgjwt,pg_graphql,pg_stat_statements,pgsodium,vault, andpgvectorfor AI workloads). - Supabase Studio — a Next.js web UI for managing schemas, running SQL, editing rows, browsing storage buckets, and configuring auth providers.
- GoTrue (Auth) — email, magic link, phone, SAML and 20+ OAuth providers for end-user authentication.
- PostgREST — automatically turns your Postgres schema into a RESTful JSON API with filtering, pagination, and joins.
- Realtime — an Elixir/Phoenix server that streams Postgres logical replication changes to connected clients via WebSocket.
- Storage API — an S3-compatible object storage layer backed by the filesystem (or MinIO/S3), with row-level security tied to your Postgres policies.
- Edge Functions — a Deno runtime for globally deployable TypeScript functions, ideal for webhooks and third-party integrations.
- Kong API Gateway — a single entry point that routes
/auth/,/rest/,/realtime/,/storage/, and/functions/*to the right service and enforces JWT validation.
docker-compose.yml file maintained by the Supabase team. You get the same SDKs (@supabase/supabase-js, the Python, Flutter, Kotlin and Swift clients) and the same Studio — only the URL changes from xxxx.supabase.co to your-domain.com.Why Self-Host Supabase Instead of Using Supabase Cloud?
Supabase Cloud is excellent for prototypes and small apps, but serious production workloads often hit limits that make self-hosting the better choice:
- No per-row, per-MAU or per-bandwidth charges. A VPS cost is fixed, whether you store 1 GB or 1 TB, whether you serve 1,000 or 1,000,000 monthly active users.
- Full data sovereignty and compliance. GDPR, HIPAA, SOC 2 audits, and EU-only data residency rules are easy to satisfy when the database physically lives in a datacenter you selected.
- Direct Postgres superuser access. You can install any extension, tweak
postgresql.conf, runpg_dumpat the filesystem level, and attach debugging tools likepgBadgerwithout waiting for vendor support. - No paused projects or cold starts. Supabase Cloud pauses inactive free-tier projects after a week. Your self-hosted stack never sleeps.
- Freedom to customise. You can swap Kong for Traefik, replace the Storage backend with MinIO or Cloudflare R2, patch GoTrue, or run multiple Supabase projects on one server.
- Predictable cost at scale. The break-even point for most SaaS apps is around 10,000 MAU or 50 GB of storage — beyond that, self-hosting on a CloudCore VPS is dramatically cheaper than Cloud Pro/Team tiers.
- Vendor independence. Should Supabase change its business model (as has happened with Parse, Heroku, and Firebase's pricing), your stack keeps running exactly as it is today.
Cost Comparison: Supabase Cloud vs. Self-Hosted
| Workload | Supabase Cloud (Pro) | Supabase Cloud (Team) | Self-Hosted on VPS |
|---|---|---|---|
| Base fee | $25/mo | $599/mo | ~EUR 24.99/mo (CloudCore Business) |
| Included compute | 2 vCPU micro | 2 vCPU small | 8 vCPU |
| Included RAM | 1 GB | 2 GB | 24 GB |
| Included storage | 8 GB | 8 GB | 200 GB NVMe |
| Bandwidth | 250 GB included | 250 GB included | Unmetered (32 TB fair use) |
| MAU limit | 100K | 100K | Unlimited |
| Daily backups | 7-day PITR | 14-day PITR | Your responsibility |
| Total for 500K MAU + 100 GB | $600+/mo | $900+/mo | EUR 24.99/mo (flat) |
Prerequisites
Before you start, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- SSH access to your server (Terminal on macOS/Linux, or PuTTY/Windows Terminal on Windows).
- At least 4 vCPU, 8 GB RAM and 40 GB of SSD storage. Supabase runs roughly 11 containers; anything smaller will swap heavily.
- A registered domain name pointed to the server's IP via an
Arecord (required for HTTPS and OAuth callback URLs). - Basic familiarity with the Linux command line and Docker concepts.
Recommended Plan: CloudCore Business>
The Supabase stack is a cluster of 11 containers — Postgres alone needs serious RAM headroom for shared buffers, WAL, and logical replication. We recommend the CloudCore Business plan:>
- 8 vCPU cores
- 24 GB RAM
- 200 GB NVMe SSD
- 32 TB bandwidth
- From EUR 24.99/month>
This gives you comfortable headroom for Postgres tuning, Realtime replication slots, Edge Function cold starts, and room to host your application stack alongside Supabase.
If you still need to install Postgres, Docker, or Nginx as standalone services elsewhere in your infrastructure, see our related guides:
Connect to your server via SSH:ssh root@your-server-ipStep 1: Prepare the Ubuntu 24.04 Server
Update the system and create a non-root user for day-to-day operations. Running Docker as root is possible, but a dedicated user reduces blast radius if a container is compromised.
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release ufw git jq opensslCreate a user named supabase (or any name you prefer) and add it to the sudo group:
sudo adduser supabase
sudo usermod -aG sudo supabaseConfigure the firewall. We only expose SSH (22), HTTP (80) and HTTPS (443) to the internet — all Supabase internal ports stay on the Docker network:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw statusExpected output:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW AnywhereReboot if the kernel was updated:
sudo rebootReconnect as the supabase user after the reboot:
ssh supabase@your-server-ipStep 2: Install Docker and Docker Compose
Supabase is distributed as a Docker Compose stack, so Docker Engine (≥ 24.0) and the Compose v2 plugin are required.
Add Docker's official GPG key and repository:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine, CLI, containerd and the Compose plugin:
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginAdd your user to the docker group so you do not need sudo for every command:
sudo usermod -aG docker $USER
newgrp dockerVerify:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7Enable the service on boot:
sudo systemctl enable --now dockerStep 3: Clone the Official Supabase Repository
Supabase ships all Docker assets in the supabase/supabase monorepo under the docker/ directory. Clone a shallow copy (you only need the current state of main):
cd ~
git clone --depth 1 https://github.com/supabase/supabase.git
cp -rp supabase/docker supabase-project
cd supabase-projectYou now have a working directory structured like this:
supabase-project/
├── docker-compose.yml # the main stack definition (11 services)
├── .env.example # template for secrets and config
├── volumes/
│ ├── api/kong.yml # Kong API gateway declarative config
│ ├── db/ # Postgres init SQL (roles, realtime, etc.)
│ ├── functions/ # Deno edge function templates
│ ├── logs/ # Vector/logflare log routing
│ └── storage/ # Storage backend mount point
└── dev/ # dev-only helpers (ignore)Copy the example env file to .env — we will fill it in next:
cp .env.example .envStep 4: Generate Secure Secrets (JWT, Anon & Service Keys)
Supabase's security model relies on a JWT secret that signs two long-lived API keys:
ANON_KEY— used by browser/mobile clients. Rows returned are filtered by Postgres Row Level Security (RLS) policies.SERVICE_ROLE_KEY— bypasses RLS. Treat it like a root password: only use it from trusted server-side code.
openssl rand -base64 40 | tr -d '\n/+=' | cut -c1-40Copy the output — you will paste it into .env as JWT_SECRET.
Next, generate the two JWTs. Supabase provides an online generator at supabase.com/docs/guides/self-hosting#api-keys, but you can also produce them locally with a small Python script:
sudo apt install -y python3-pip
pip3 install --break-system-packages pyjwtCreate a helper file:
cat > ~/gen-keys.py <<'EOF' import jwt, sys, time secret = sys.argv[1] now = int(time.time())10-year expiry for self-hosted use
exp = now + (60 60 24 365 10)
anon = jwt.encode( {"role": "anon", "iss": "supabase", "iat": now, "exp": exp}, secret, algorithm="HS256" ) service = jwt.encode( {"role": "service_role", "iss": "supabase", "iat": now, "exp": exp}, secret, algorithm="HS256" ) print("ANON_KEY=" + anon) print("SERVICE_ROLE_KEY=" + service) EOF
Run it with your JWT secret:
python3 ~/gen-keys.py "your-40-char-jwt-secret-here"Expected output:
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Save both values — you will paste them into .env.
Finally, generate passwords for the Postgres superuser and Studio dashboard:
# Postgres password
openssl rand -base64 32 | tr -d '\n/+=' | cut -c1-32Studio dashboard password
openssl rand -base64 24 | tr -d '\n/+=' | cut -c1-24Step 5: Configure the .env File
Open the .env file you copied earlier and fill in the secrets you just generated:
nano .envHere are the key variables to set. Anything not shown can be left at its default:
############
Secrets
############
POSTGRES_PASSWORD=your-32-char-postgres-password
JWT_SECRET=your-40-char-jwt-secret
ANON_KEY=eyJhbGciOiJI...your-anon-jwt
SERVICE_ROLE_KEY=eyJhbGciOiJI...your-service-role-jwt
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=your-24-char-studio-password############
Database
############
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432############
API Proxy (Kong)
############
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443############
API — public URL clients will hit
############
SITE_URL=https://supabase.yourdomain.com
SUPABASE_PUBLIC_URL=https://supabase.yourdomain.com
API_EXTERNAL_URL=https://supabase.yourdomain.com
ADDITIONAL_REDIRECT_URLS=############
Auth (GoTrue)
############
DISABLE_SIGNUP=false
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_ANONYMOUS_USERS=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false############
SMTP — required for password resets and confirmations
############
[email protected]
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASS=your-smtp-password
SMTP_SENDER_NAME=Supabase############
Studio
############
STUDIO_DEFAULT_ORGANIZATION=MyOrg
STUDIO_DEFAULT_PROJECT=Default Project
STUDIO_PORT=3000############
Edge Functions
############
FUNCTIONS_VERIFY_JWT=false############
Realtime — left as default, uses logical replication on the db container
########################
Storage
############
STORAGE_BACKEND=file
GLOBAL_S3_BUCKET=
REGION=localImportant: TheSITE_URLandAPI_EXTERNAL_URLmust match the public HTTPS URL clients will use. OAuth callbacks, password reset links, and realtime subscriptions all rely on this value. If you change it later, restart the stack.
Save and exit (Ctrl+O, Enter, Ctrl+X).
Step 6: Start the Supabase Stack
Pull all images first — this can take several minutes depending on your bandwidth:
docker compose pullThen start the stack in detached mode:
docker compose up -dExpected output (abbreviated):
[+] Running 11/11
✔ Container supabase-db Healthy
✔ Container supabase-vector Started
✔ Container supabase-analytics Healthy
✔ Container supabase-auth Started
✔ Container supabase-rest Started
✔ Container supabase-realtime Started
✔ Container supabase-storage Started
✔ Container supabase-imgproxy Started
✔ Container supabase-meta Started
✔ Container supabase-edge-functions Started
✔ Container supabase-kong Started
✔ Container supabase-studio StartedCheck running containers:
docker compose psAll 11 services should be in state running (some with healthy health checks). If any container is restarting, view its logs:
docker compose logs -f <service-name>Common first-boot issue: the db container takes 30-60 seconds to finish initial migrations before auth, rest, and realtime can connect. Give it a minute before declaring anything broken.
Step 7: Verify Each Service
Test each internal service through Kong on port 8000 (still localhost at this stage — we will add HTTPS next):
PostgREST (REST API)
curl http://localhost:8000/rest/v1/ \
-H "apikey: $ANON_KEY"Expected output:
{"swagger":"2.0","info":{"description":"...","title":"PostgREST API","version":"12.0.1"}, ...}Auth (GoTrue)
curl http://localhost:8000/auth/v1/healthExpected output:
{"version":"v2.160.0","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}Realtime
curl http://localhost:8000/realtime/v1/api/tenants/realtime-dev/health \
-H "apikey: $ANON_KEY"Expected output:
{"healthy":true}Storage
curl http://localhost:8000/storage/v1/bucket \
-H "apikey: $SERVICE_ROLE_KEY" \
-H "Authorization: Bearer $SERVICE_ROLE_KEY"Expected output (empty array on a fresh install):
[]Edge Functions
curl http://localhost:8000/functions/v1/hello \
-H "Authorization: Bearer $ANON_KEY"You will get a 404 until you deploy a function — that is expected. The gateway is routing correctly.
Studio
Studio is exposed on port 3000 behind basic auth. Try it via SSH tunnel:
ssh -L 3000:localhost:3000 supabase@your-server-ipOpen http://localhost:3000 in your browser. You should see the Supabase Studio login — enter the DASHBOARD_USERNAME and DASHBOARD_PASSWORD from .env.
Step 8: Configure the Kong API Gateway
Kong is already running with a sensible default configuration from volumes/api/kong.yml. It declaratively defines:
- Routes for
/auth/v1/,/rest/v1/,/realtime/v1/,/storage/v1/,/functions/v1/,/pg/,/analytics/v1/*. - JWT validation on protected routes using your
anonandservice_rolekeys. - CORS headers for browser clients.
- Rate limiting plugin stubs you can enable.
cat volumes/api/kong.yml | head -80Key sections you may want to customise:
Rate limiting. Add a plugins block to any service to throttle abusive clients. Example — cap anonymous REST requests at 60/minute:
services:
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: rate-limiting
config:
minute: 60
policy: localCustom CORS. Edit the cors plugin at the top of kong.yml to restrict Access-Control-Allow-Origin to your frontend domain:
plugins:
- name: cors
config:
origins:
- https://app.yourdomain.com
methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
- PATCH
credentials: true
max_age: 3600After editing kong.yml, reload Kong without downtime:
docker compose restart kongKong re-parses the declarative config at startup. You can verify by hitting an endpoint and checking the response headers for the new CORS origin.
Step 9: Expose Studio via Nginx with HTTPS
Right now every Supabase service listens only on 127.0.0.1. We will put Nginx in front as a reverse proxy on the host, serve Studio and the public API over HTTPS, and obtain a free Let's Encrypt certificate.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate a new Nginx site at /etc/nginx/sites-available/supabase:
sudo tee /etc/nginx/sites-available/supabase > /dev/null <<'EOF'Redirect HTTP to HTTPS
server { listen 80; server_name supabase.yourdomain.com; return 301 https://$host$request_uri; }Public API and Studio over HTTPS
server { listen 443 ssl http2; server_name supabase.yourdomain.com;# Certbot will fill these in ssl_certificate /etc/letsencrypt/live/supabase.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/supabase.yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers add_header X-Content-Type-Options nosniff; add_header X-Frame-Options DENY; add_header Referrer-Policy no-referrer-when-downgrade;
client_max_body_size 50m;
# Route Studio (HTML UI) at / location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
# Route the Supabase API (Kong) at /auth, /rest, /realtime, /storage, /functions location ~ ^/(auth|rest|realtime|storage|functions|analytics|pg)/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for Realtime proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } } EOF
Enable the site and validate:
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -tObtain an SSL certificate with Certbot (it handles renewing Nginx config automatically):
sudo certbot --nginx -d supabase.yourdomain.com
sudo systemctl reload nginxCertbot installs a systemd timer that renews the certificate twice daily. Verify:
sudo systemctl list-timers | grep certbotNow open https://supabase.yourdomain.com in a browser — you should see Studio with a valid padlock icon.
Step 10: Test Auth, PostgREST, Realtime, Storage and Edge Functions
With HTTPS in place, test each service through its public URL. Replace supabase.yourdomain.com and the keys with your own values.
Sign up a user (Auth)
curl -X POST https://supabase.yourdomain.com/auth/v1/signup \
-H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "StrongPassword123!"
}'Expected output includes an access_token, a refresh_token, and a user object with a UUID.
Create a table and query it (PostgREST)
In Studio, open the SQL editor and run:
create table public.todos ( id bigserial primary key, task text not null, done boolean default false, created_at timestamptz default now() );alter table public.todos enable row level security;
create policy "anyone can read todos" on public.todos for select using (true);
insert into public.todos (task) values ('Ship Supabase self-hosted');
Then from your terminal:
curl "https://supabase.yourdomain.com/rest/v1/todos?select=*" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ANON_KEY"Expected output:
[{"id":1,"task":"Ship Supabase self-hosted","done":false,"created_at":"2026-04-16T10:00:00Z"}]Subscribe to realtime changes
Using the JavaScript SDK (install with npm install @supabase/supabase-js):
import { createClient } from '@supabase/supabase-js'const supabase = createClient( 'https://supabase.yourdomain.com', 'YOUR_ANON_KEY' )
supabase .channel('todos') .on('postgres_changes', { event: '*', schema: 'public', table: 'todos' }, (payload) => console.log('Change:', payload)) .subscribe()
Any INSERT, UPDATE, or DELETE on the todos table streams to your client via WebSocket.
Upload a file (Storage)
# Create a bucket
curl -X POST https://supabase.yourdomain.com/storage/v1/bucket \
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
-H "Content-Type: application/json" \
-d '{"id":"public-assets","name":"public-assets","public":true}'Upload a file
echo "hello supabase" > demo.txt
curl -X POST https://supabase.yourdomain.com/storage/v1/object/public-assets/demo.txt \
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
--data-binary @demo.txtFetch the public URL
curl https://supabase.yourdomain.com/storage/v1/object/public/public-assets/demo.txtDeploy an Edge Function
Install the Supabase CLI locally (on your laptop, not the server):
npm install -g supabase
supabase loginCreate and deploy a function:
supabase functions new hello
Edit supabase/functions/hello/index.ts
supabase functions deploy hello --project-ref your-self-hosted-ref \
--no-verify-jwtFor self-hosted deployments, copy the function directory into volumes/functions/hello/ on the server and restart the edge runtime:
docker compose restart functionsThen call it:
curl https://supabase.yourdomain.com/functions/v1/hello \
-H "Authorization: Bearer $ANON_KEY"Backups and Upgrades
Automated Postgres Backups
The db container persists data to the supabase-project/volumes/db/data directory. For production, schedule nightly logical backups to off-server storage.
Create a backup script:
sudo tee /usr/local/bin/supabase-backup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR=/var/backups/supabase
mkdir -p "$BACKUP_DIR"docker compose -f /home/supabase/supabase-project/docker-compose.yml exec -T db \
pg_dumpall -U postgres | gzip > "$BACKUP_DIR/supabase-$STAMP.sql.gz"
Keep last 14 days
find "$BACKUP_DIR" -name 'supabase-*.sql.gz' -mtime +14 -delete
EOF
sudo chmod +x /usr/local/bin/supabase-backup.shAdd a cron entry at 02:00 daily:
sudo crontab -e
Add:
0 2 * /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1For off-site safety, pipe the gzipped output to rclone or aws s3 cp to push it to S3, Backblaze B2 or a second VPS.
Upgrading Supabase
Pull the latest images and recreate containers:
cd ~/supabase-project
docker compose pull
docker compose up -dSupabase migrations on the db container run automatically at startup. Always back up before upgrading — major Postgres version jumps may require a dump/restore rather than an in-place upgrade.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
supabase-db container keeps restarting | Stale volume from a previous .env with a different POSTGRES_PASSWORD | Stop stack, delete volumes/db/data, restart. You will lose data — only do this on a fresh install. |
auth logs show connection refused: db:5432 | db is still initialising | Wait 60 seconds. If it persists, check docker compose logs db for crash reasons. |
| Studio shows "Failed to fetch" on the SQL editor | meta service crashed or wrong SUPABASE_PUBLIC_URL | Restart meta: docker compose restart meta. Verify SUPABASE_PUBLIC_URL matches your HTTPS URL. |
| Realtime subscriptions never receive events | Logical replication not enabled on the publication | Run SELECT * FROM pg_publication; in SQL editor. If empty, connect to db and run CREATE PUBLICATION supabase_realtime FOR ALL TABLES; |
JWT expired errors from PostgREST | Server clock drift | sudo timedatectl set-ntp true and restart. |
Edge function returns 401 Missing authorization header | FUNCTIONS_VERIFY_JWT=true but no bearer token passed | Either pass Authorization: Bearer $ANON_KEY or set FUNCTIONS_VERIFY_JWT=false in .env. |
kong container healthy but all requests return 404 | kong.yml syntax error silently ignored | docker compose logs kong \</td><td>grep -i error<code>. Validate YAML, then </code>docker compose restart kong. |
Nginx returns 502 Bad Gateway for /realtime/ | Missing WebSocket upgrade headers | Ensure the proxy_set_header Upgrade and Connection "upgrade" lines are present in the location block. |
| Out of memory — containers OOM-killed | 4 GB VPS too small for the full stack | Upgrade to at least 8 GB RAM, or disable analytics and vector containers if you do not need log routing. |
Useful log commands
# Tail all services
docker compose logs -fJust one service
docker compose logs -f authPostgres slow queries
docker compose exec db psql -U postgres -c \
"SELECT query, calls, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"Disk usage per container volume
docker system df -vFAQ
Can I run self-hosted Supabase alongside my existing app on the same server?
Yes, and it is a common pattern. Supabase listens only on 127.0.0.1 by default, and only Nginx on 80/443 is exposed. You can run a Next.js/Nuxt/Django app on another internal port (say 3001) and add another Nginx server {} block for its own subdomain, all on the same VPS. On a CloudCore Business plan with 24 GB RAM, Supabase uses around 4-6 GB at idle, leaving plenty for your app, Redis, queues, and a reverse proxy.
How do I migrate an existing Supabase Cloud project to self-hosted?
Supabase Cloud lets you download a full Postgres dump from the dashboard: Project Settings → Database → Backups → Download. Transfer the .sql file to your VPS and restore it into the self-hosted db container with docker compose exec -T db psql -U postgres < dump.sql. You then re-apply any storage buckets by copying them from the Cloud via the CLI and updating any hardcoded Cloud URLs in your client to your new SUPABASE_PUBLIC_URL. Auth users migrate as part of the Postgres dump since they live in the auth.users table.
Is self-hosted Supabase production-ready?
Yes. The Docker Compose stack you deployed runs the exact same container images that power Supabase Cloud. Production hardening is your responsibility though: schedule backups, put the stack behind a firewall, enable Postgres ssl, rotate JWT keys periodically, monitor with Prometheus/Grafana, and upgrade regularly. For mission-critical workloads consider running a hot-standby replica on a second VPS using Postgres streaming replication.
What is the difference between the anon key and the service role key?
The anon key is safe to embed in browser and mobile clients. Every request it authorises is subject to Row Level Security (RLS) policies you define in Postgres — the database itself decides which rows the request can see. The service role key bypasses RLS entirely and grants full database privileges. Never ship it to a client. Use it only in server-side code, cron jobs, and admin tools. If a service role key leaks, rotate JWT_SECRET immediately and regenerate both keys, then restart the stack.
Can I use a managed Postgres (like AWS RDS or Neon) instead of the bundled db container?
Yes. Point POSTGRES_HOST, POSTGRES_PASSWORD, and POSTGRES_DB in .env at your external Postgres, remove the db service from docker-compose.yml, and ensure the required extensions (pgcrypto, pgjwt, pg_graphql, pgsodium, pgvector) are installed there. You lose the convenience of one-command startup, but you gain managed backups, HA failover, and point-in-time recovery.
How do I add OAuth providers like Google or GitHub?
Add the relevant variables to .env. For example, Google:
GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=your-client-id
GOTRUE_EXTERNAL_GOOGLE_SECRET=your-client-secret
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://supabase.yourdomain.com/auth/v1/callbackRestart the auth container: docker compose restart auth. Then in your Google Cloud Console, add the redirect URI shown above to the OAuth client's allow-list. Repeat for GitHub, GitLab, Azure, Discord, and the 20+ other supported providers.
Next Steps
Now that Supabase is running on your VPS, here are practical next steps to get the most out of it:
- Wire up your frontend. Install
@supabase/supabase-jsin your Next.js, Nuxt, SvelteKit, or React Native app and point it athttps://supabase.yourdomain.comwith your anon key. The SDK is 100% API-compatible with Supabase Cloud. - Tune Postgres. The default container ships with conservative settings. Edit
volumes/db/postgres.confor pass environment variables to increaseshared_buffers,effective_cache_size, andwork_memto match your VPS RAM. - Install pgvector for AI. Supabase's Postgres image includes
pgvector. Runcreate extension vector;to start storing OpenAI, Cohere, or local embeddings alongside your relational data — perfect for RAG systems. - Set up monitoring. Scrape Postgres with
postgres_exporter, Kong with its Prometheus plugin, and the Docker daemon withcadvisor. Visualise in Grafana to catch slow queries and memory leaks before users do. - Deploy a CI/CD pipeline for Edge Functions. Store your functions in Git and push them to
volumes/functions/via GitHub Actions and SSH — no proprietary deploy platform required. - Add rate limiting and WAF. Enable Kong's
rate-limiting,bot-detection, andip-restrictionplugins to protect your public endpoints from abuse. - Scale horizontally. When one VPS is not enough, run Postgres on a dedicated database VPS, and put the stateless services (auth, rest, realtime, storage, functions, kong, studio) behind a load balancer on two or more app VPSes.
Skip the infrastructure setup — start with a backend-ready VPS>
Our CloudCore Business plan is sized precisely for backend stacks like Supabase, with 8 vCPU, 24 GB RAM and 200 GB NVMe storage on EU-hosted infrastructure.>
- Ubuntu 24.04 LTS pre-installed
- Unmetered-tier bandwidth (32 TB fair use)
- Full root access and snapshot backups
- IPv4 + IPv6 included>
Deploy your CloudCore Business VPS — from EUR 24.99/month.