Supabase has become the default open-source alternative to Firebase, offering PostgreSQL as a database, built-in authentication, instant REST and GraphQL APIs, realtime subscriptions, edge functions, and file storage — all behind a single dashboard. The managed Supabase Cloud service works well for prototyping, but production workloads hit free tier limits quickly: 500MB database, 1GB file storage, 2GB bandwidth, and 50,000 monthly active users. Beyond those limits, pricing scales with usage in ways that are difficult to predict. Self-hosting Supabase on your own Ubuntu VPS eliminates every one of those constraints while giving you full control over your data.
Running Supabase yourself means no vendor lock-in, complete data sovereignty, predictable monthly costs regardless of traffic, and the ability to customize every component. The trade-off is operational responsibility — you manage updates, backups, and scaling. This guide walks through the entire process: from understanding the architecture to deploying a production-ready Supabase instance with SSL, proper security, and backup automation.
MassiveGRID Ubuntu VPS includes: Ubuntu 24.04 LTS pre-installed · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · Independent CPU/RAM/storage scaling · 12 Tbps DDoS protection · 4 global datacenter locations · 100% uptime SLA · 24/7 human support rated 9.5/10
Deploy a self-managed VPS — from $1.99/mo
Need dedicated resources? — from $19.80/mo
Want fully managed hosting? — we handle everything
Why Self-Host Supabase
The case for self-hosting goes beyond cost savings. Here are the concrete advantages:
- No usage limits — Database size, file storage, bandwidth, and active users are bounded only by your server resources, not by a pricing tier.
- Data sovereignty — Your PostgreSQL database lives on infrastructure you control, in a datacenter you choose. Critical for GDPR, HIPAA, or any regulation requiring data residency.
- Predictable costs — A VPS with 4 vCPU and 16GB RAM costs the same whether you serve 1,000 or 100,000 API requests per day.
- No vendor lock-in — Supabase is built on PostgreSQL. Your data is in a standard database format that works with any tool or platform.
- Full customization — Modify PostgreSQL extensions, tune connection pooling, add custom authentication providers, or run database migrations on your schedule.
- Network locality — Run Supabase in the same datacenter as your application servers for sub-millisecond latency between your app and its backend.
Supabase Cloud vs Self-Hosted: An Honest Comparison
Self-hosting is not universally better. Here is what you gain and what you lose:
What you gain: Unlimited database size, unlimited bandwidth, unlimited active users, full PostgreSQL superuser access, custom extensions, data residency control, no per-project pricing, and the ability to run multiple projects on one instance.
What you lose: Automatic updates, managed backups, the Supabase dashboard's project management features, built-in log drains, branching (preview environments), edge function deployment infrastructure, and official support. You also lose the convenience of one-click setup — self-hosting requires understanding Docker, networking, and PostgreSQL administration.
The general rule: use Supabase Cloud for prototypes and small projects. Self-host when you need predictable pricing at scale, data sovereignty, or deep PostgreSQL customization.
Understanding Supabase Architecture
Supabase is not a single application. It is a composition of 10+ services, each handling a specific concern. Understanding what each service does helps you troubleshoot issues and tune performance:
- PostgreSQL — The core database. Everything else is built around it.
- PostgREST — Automatically generates a RESTful API from your PostgreSQL schema. Every table becomes an endpoint.
- GoTrue — Authentication service. Handles sign-up, sign-in, OAuth providers, magic links, and JWT token management.
- Realtime — Listens to PostgreSQL's WAL (Write-Ahead Log) and broadcasts changes to connected WebSocket clients.
- Storage API — S3-compatible file storage backed by a local filesystem or S3-compatible object store.
- Kong — API gateway that routes all external requests to the correct internal service and handles rate limiting.
- Studio — The web-based dashboard for managing your database, authentication, storage, and API settings.
- Meta — Serves metadata about your PostgreSQL database to Studio.
- pg_meta — PostgreSQL management API used by Studio for schema operations.
- Imgproxy — On-the-fly image resizing and optimization for the Storage API.
- Analytics — Log collection using BigQuery-compatible engine (Logflare).
In Docker, each service runs as its own container. This is why Supabase needs more RAM than a typical application — you are running an entire backend-as-a-service platform.
Prerequisites
Supabase runs 10+ Docker containers simultaneously. Minimum requirements for a development or staging environment: 4 vCPU and 8GB RAM. For production workloads, allocate 4 vCPU and 16GB RAM with room to scale. A Cloud VPS with independent resource scaling lets you add RAM without migrating.
You need Docker and Docker Compose installed. If you have not set those up yet, follow our Docker installation guide for Ubuntu VPS first.
Verify your Docker installation:
docker --version
docker compose version
You also need a domain name pointed to your server's IP address, and Git installed to clone the Supabase repository:
sudo apt update
sudo apt install -y git
Docker Compose Setup
Supabase provides an official self-hosting configuration. Clone the repository and use the Docker directory:
# Clone the Supabase repository
git clone --depth 1 https://github.com/supabase/supabase.git /opt/supabase
cd /opt/supabase/docker
# Copy the example environment file
cp .env.example .env
The docker directory contains the docker-compose.yml and configuration files for all services. Before starting anything, you must configure the environment variables — running with defaults is a security risk.
Environment Configuration
The .env file controls every aspect of your Supabase deployment. Open it and configure these critical variables:
# /opt/supabase/docker/.env
# ── Secrets (CHANGE ALL OF THESE) ──────────────────────────────
# Generate each with: openssl rand -base64 32
POSTGRES_PASSWORD=your-super-secret-postgres-password
JWT_SECRET=your-super-secret-jwt-token-at-least-32-characters
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=your-dashboard-password
# ── API Configuration ──────────────────────────────────────────
SITE_URL=https://supabase.yourdomain.com
API_EXTERNAL_URL=https://supabase.yourdomain.com
SUPABASE_PUBLIC_URL=https://supabase.yourdomain.com
# ── Database ───────────────────────────────────────────────────
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432
# ── Studio ─────────────────────────────────────────────────────
STUDIO_DEFAULT_ORGANIZATION=My Organization
STUDIO_DEFAULT_PROJECT=My Project
STUDIO_PORT=3000
# ── Auth (GoTrue) ─────────────────────────────────────────────
GOTRUE_SITE_URL=https://yourdomain.com
GOTRUE_EXTERNAL_EMAIL_ENABLED=true
GOTRUE_MAILER_AUTOCONFIRM=false
GOTRUE_SMTP_HOST=smtp.yourdomain.com
GOTRUE_SMTP_PORT=587
GOTRUE_SMTP_USER=noreply@yourdomain.com
GOTRUE_SMTP_PASS=your-smtp-password
GOTRUE_SMTP_SENDER_NAME=Your App
# ── Storage ────────────────────────────────────────────────────
STORAGE_BACKEND=file
FILE_SIZE_LIMIT=52428800 # 50MB
Generate the JWT keys using the Supabase CLI or the JWT generation tool. The ANON_KEY is a JWT token with the anon role, and the SERVICE_ROLE_KEY is a JWT with the service_role role. Both must be signed with your JWT_SECRET:
# Generate JWT tokens (requires Node.js or use an online tool)
# Payload for ANON_KEY:
# { "role": "anon", "iss": "supabase", "iat": 1735689600, "exp": 1893456000 }
# Payload for SERVICE_ROLE_KEY:
# { "role": "service_role", "iss": "supabase", "iat": 1735689600, "exp": 1893456000 }
# Generate with openssl:
JWT_SECRET=$(openssl rand -base64 32)
echo "JWT_SECRET: $JWT_SECRET"
Now start the stack:
cd /opt/supabase/docker
docker compose up -d
Watch the logs during initial startup to verify all services come up healthy:
docker compose logs -f --tail=50
The first start takes 2-5 minutes as containers pull images and PostgreSQL initializes. Check service health:
docker compose ps
All services should show as "healthy" or "running."
Nginx Reverse Proxy with SSL
Supabase's Kong API gateway listens on port 8000 by default, and Studio on port 3000. You need a reverse proxy with SSL termination to expose these securely. If you do not already have Nginx configured, follow our Nginx reverse proxy guide and SSL certificate installation guide.
# /etc/nginx/sites-available/supabase
server {
listen 80;
server_name supabase.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name supabase.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/supabase.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/supabase.yourdomain.com/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Kong API Gateway — handles all API routes
location / {
proxy_pass http://127.0.0.1:8000;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
# Studio dashboard (optional — restrict access)
server {
listen 443 ssl http2;
server_name studio.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/studio.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/studio.yourdomain.com/privkey.pem;
# Restrict to your IP
allow 203.0.113.50;
deny all;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
The WebSocket upgrade headers are critical — without them, Supabase Realtime will not work. The proxy_read_timeout 86400 keeps WebSocket connections alive for up to 24 hours.
First Login to Studio Dashboard
Navigate to your Studio URL (e.g., https://studio.yourdomain.com). You will be prompted for the credentials you set in DASHBOARD_USERNAME and DASHBOARD_PASSWORD.
Once logged in, Studio shows your project overview with sections for the Table Editor, SQL Editor, Authentication, Storage, and Edge Functions. The Table Editor provides a spreadsheet-like interface for viewing and editing database records. The SQL Editor lets you run raw queries and save them as snippets.
Verify the connection is working by opening the SQL Editor and running:
SELECT version();
SELECT current_database();
SELECT * FROM pg_extension;
You should see PostgreSQL 15+ with extensions like uuid-ossp, pgcrypto, and pgjwt already installed.
Creating Your First Project
Unlike Supabase Cloud, self-hosted Supabase does not have multi-project support by default — you get one PostgreSQL database. Structure your application using schemas:
-- Create a table in the public schema
CREATE TABLE public.profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Policy: users can read all profiles
CREATE POLICY "Profiles are viewable by everyone"
ON public.profiles
FOR SELECT
USING (true);
-- Policy: users can update their own profile
CREATE POLICY "Users can update own profile"
ON public.profiles
FOR UPDATE
USING (auth.uid() = id);
-- Enable realtime for this table
ALTER PUBLICATION supabase_realtime ADD TABLE public.profiles;
For file storage, create a bucket via the Studio dashboard or the Storage API:
-- Create a storage bucket for avatars
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
-- Policy: authenticated users can upload
CREATE POLICY "Avatar upload"
ON storage.objects
FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.role() = 'authenticated');
Connecting Your Application
Install the Supabase JavaScript SDK in your application:
npm install @supabase/supabase-js
Initialize the client pointing to your self-hosted instance:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://supabase.yourdomain.com',
'your-anon-key' // The ANON_KEY from your .env
)
// Sign up a user
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password'
})
// Query data
const { data: profiles } = await supabase
.from('profiles')
.select('*')
.limit(10)
// Subscribe to realtime changes
const channel = supabase
.channel('profiles-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'profiles' },
(payload) => console.log('Change:', payload)
)
.subscribe()
You can also use the REST API directly without the SDK:
# Query profiles via REST
curl -X GET 'https://supabase.yourdomain.com/rest/v1/profiles?select=*' \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
# Insert a record
curl -X POST 'https://supabase.yourdomain.com/rest/v1/profiles' \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_USER_JWT" \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "avatar_url": "https://example.com/avatar.jpg"}'
Securing Your Deployment
A self-hosted Supabase instance requires deliberate security hardening. The defaults are designed for development, not production:
Rotate default secrets immediately:
# Generate new secrets
openssl rand -base64 32 # For JWT_SECRET
openssl rand -base64 24 # For POSTGRES_PASSWORD
openssl rand -base64 24 # For DASHBOARD_PASSWORD
Restrict network access:
# Only expose ports through Nginx — block direct access
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 8000 # Kong
sudo ufw deny 3000 # Studio
sudo ufw deny 5432 # PostgreSQL
sudo ufw enable
Configure Docker to not bypass UFW:
# /etc/docker/daemon.json
{
"iptables": false
}
# Restart Docker after this change
sudo systemctl restart docker
Disable public sign-ups if your application handles registration differently:
# In .env
GOTRUE_DISABLE_SIGNUP=true
Set rate limits in Kong: Edit the Kong configuration in volumes/api/kong.yml to add rate limiting plugins to your routes, preventing API abuse.
For comprehensive server hardening beyond Supabase, follow our Ubuntu VPS security hardening guide.
PostgreSQL Tuning for Supabase Workloads
The default PostgreSQL configuration inside the Supabase Docker container is conservative. For production workloads, mount a custom configuration. For detailed PostgreSQL optimization, see our PostgreSQL guide.
Create a custom PostgreSQL config that the Docker container will use:
# /opt/supabase/docker/volumes/db/postgresql.conf (appended settings)
# Memory — allocate 25% of total RAM to shared_buffers
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 64MB
maintenance_work_mem = 512MB
# WAL — important for Realtime service
wal_level = logical
max_wal_senders = 10
max_replication_slots = 10
# Connections — PostgREST and other services use connection pooling
max_connections = 200
# Query planner
random_page_cost = 1.1 # NVMe storage
effective_io_concurrency = 200 # NVMe storage
default_statistics_target = 100
After modifying the configuration, restart the database container:
cd /opt/supabase/docker
docker compose restart db
Monitor query performance from Studio's SQL Editor:
-- Enable pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Find slow queries
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
Backup Strategy
Self-hosting means you own backups. Your Supabase instance has two categories of data to protect: the PostgreSQL database and the file storage objects.
Database backups with pg_dump:
#!/bin/bash
# /opt/supabase/backup.sh
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/supabase/backups"
mkdir -p "$BACKUP_DIR"
# Dump the entire database
docker compose -f /opt/supabase/docker/docker-compose.yml exec -T db \
pg_dump -U postgres -Fc --no-owner postgres \
> "$BACKUP_DIR/supabase_db_$TIMESTAMP.dump"
# Backup storage objects
tar -czf "$BACKUP_DIR/supabase_storage_$TIMESTAMP.tar.gz" \
/opt/supabase/docker/volumes/storage/
# Remove backups older than 30 days
find "$BACKUP_DIR" -name "*.dump" -mtime +30 -delete
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
echo "Backup completed: $TIMESTAMP"
# Make executable and schedule
chmod +x /opt/supabase/backup.sh
# Run daily at 3 AM
crontab -e
# Add: 0 3 * * * /opt/supabase/backup.sh >> /var/log/supabase-backup.log 2>&1
For automated off-server backup strategies including encryption and remote storage, see our backup automation guide.
Restore from backup:
# Restore database
docker compose -f /opt/supabase/docker/docker-compose.yml exec -T db \
pg_restore -U postgres -d postgres --clean --no-owner \
< /opt/supabase/backups/supabase_db_20260228_030000.dump
Resource Monitoring and Scaling
Different Supabase services consume resources differently. Understanding the profile helps you scale the right component:
- PostgreSQL — Heaviest consumer. RAM for shared_buffers and active queries, CPU for complex queries. Scale RAM first.
- PostgREST — Lightweight. ~50-100MB RAM. Scales with concurrent API requests.
- GoTrue — Lightweight. ~50MB RAM. Spikes during authentication bursts.
- Realtime — RAM scales with connected WebSocket clients. 1,000 connections ≈ 200-500MB.
- Storage API — CPU for image processing (imgproxy), disk I/O for file serving.
- Kong — ~100-200MB RAM. CPU scales with request throughput.
- Studio — ~200-400MB RAM. Only used by administrators — can be stopped when not needed.
Monitor overall resource usage:
# Container-level resource usage
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
# Check PostgreSQL connections
docker compose exec db psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
# Check disk usage
du -sh /opt/supabase/docker/volumes/*/
Your entire backend runs on one server. Dedicated VPS resources ensure consistent performance across all Supabase services — no noisy neighbors affecting database query latency or realtime WebSocket throughput.
Prefer Managed Supabase Hosting?
Self-hosted Supabase involves managing PostgreSQL, GoTrue, PostgREST, Realtime, Storage, Kong, and Studio — each with its own update cadence, configuration requirements, and failure modes. PostgreSQL alone requires monitoring replication slots, WAL size, connection counts, and query performance. Multiply that across every service in the stack.
MassiveGRID's fully managed hosting handles the infrastructure layer: server administration, security patches, performance tuning, backup verification, and 24/7 monitoring. You deploy and configure Supabase. We keep the server running, secure, and optimized. That division of responsibility gives you the benefits of self-hosting without the undifferentiated heavy lifting of server management.