Supabase Self-Hosted Installation

Supabase is an open-source Firebase alternative that provides a PostgreSQL database, authentication, real-time subscriptions, file storage, and auto-generated APIs in a single deployable platform. Self-hosting Supabase on your own Linux server gives you full data ownership, no per-row pricing, and the ability to customize every component.

Prerequisites

  • A Linux VPS with at least 4 GB RAM (8 GB recommended for production)
  • Ubuntu 20.04+ or Debian 11+
  • Docker 20.10+ and Docker Compose v2+
  • A domain name with DNS configured
  • Ports 80 and 443 open

Setting Up Supabase with Docker

# Clone the Supabase self-hosted repository
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/docker

# Copy the example environment file
cp .env.example .env

# Generate required secrets
# JWT Secret (at least 32 characters)
openssl rand -hex 32

# Postgres password
openssl rand -hex 16

# Dashboard password
openssl rand -hex 12

Edit the .env file with your generated values:

# Key values to configure in .env
POSTGRES_PASSWORD=your-super-secret-postgres-password
JWT_SECRET=your-super-secret-jwt-token-at-least-32-characters
ANON_KEY=<generated-anon-jwt>
SERVICE_ROLE_KEY=<generated-service-role-jwt>
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=your-dashboard-password
SITE_URL=https://your-app-domain.com
API_EXTERNAL_URL=https://api.your-domain.com
SUPABASE_PUBLIC_URL=https://api.your-domain.com

Generate JWT tokens using the JWT secret:

# Install jwt-cli or use a script to generate the required JWTs
# The anon and service_role keys are JWTs signed with JWT_SECRET

# Example using Node.js
node -e "
const jwt = require('jsonwebtoken');
const secret = 'your-jwt-secret';
const anon = jwt.sign({role: 'anon', iss: 'supabase'}, secret);
const service = jwt.sign({role: 'service_role', iss: 'supabase'}, secret);
console.log('ANON_KEY=' + anon);
console.log('SERVICE_ROLE_KEY=' + service);
"

Start the Supabase stack:

# Pull all images
docker compose pull

# Start all services in background
docker compose up -d

# Verify all containers are running
docker compose ps

# Expected services:
# supabase-db (PostgreSQL)
# supabase-api (PostgREST)
# supabase-auth (GoTrue)
# supabase-realtime
# supabase-storage
# supabase-studio (dashboard)
# supabase-kong (API gateway)
# supabase-functions

Configuring PostgreSQL

Supabase uses PostgreSQL with several extensions pre-installed:

# Connect to Postgres directly
docker exec -it supabase-db psql -U postgres

# Or using psql with the exposed port
psql "postgresql://postgres:your-password@localhost:5432/postgres"

# Check installed extensions
SELECT * FROM pg_extension;

# Key extensions available:
# pgcrypto - encryption functions
# uuid-ossp - UUID generation
# pgjwt - JWT signing in SQL
# pg_stat_statements - query analytics

Create application tables with Row Level Security:

-- Create a table
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users ON DELETE CASCADE,
  username TEXT UNIQUE,
  avatar_url TEXT,
  updated_at TIMESTAMP WITH TIME ZONE,
  PRIMARY KEY (id)
);

-- Enable Row Level Security
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Policy: users can only read their own profile
CREATE POLICY "Users can view own profile"
  ON public.profiles FOR SELECT
  USING (auth.uid() = id);

-- Policy: users can update their own profile
CREATE POLICY "Users can update own profile"
  ON public.profiles FOR UPDATE
  USING (auth.uid() = id);

Authentication Setup

GoTrue handles authentication in Supabase. Configure providers in .env:

# Email/password auth (enabled by default)
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=false  # Set true for development

# Google OAuth
GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=your-google-client-id
GOTRUE_EXTERNAL_GOOGLE_SECRET=your-google-client-secret

# GitHub OAuth
GOTRUE_EXTERNAL_GITHUB_ENABLED=true
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=your-github-client-id
GOTRUE_EXTERNAL_GITHUB_SECRET=your-github-client-secret

# SMTP for email delivery
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-sendgrid-api-key
[email protected]
SMTP_SENDER_NAME=MyApp

After updating .env, restart auth service:

docker compose restart supabase-auth

Storage Configuration

Supabase Storage manages file uploads with access policies:

# Storage uses local disk by default
# Configure S3-compatible storage in .env:
STORAGE_BACKEND=s3
GLOBAL_S3_BUCKET=your-bucket-name
AWS_DEFAULT_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key

# For MinIO (self-hosted S3):
STORAGE_BACKEND=s3
GLOBAL_S3_BUCKET=supabase-storage
AWS_DEFAULT_REGION=us-east-1
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
STORAGE_S3_ENDPOINT=http://minio:9000
STORAGE_S3_FORCE_PATH_STYLE=true

Create storage buckets via the API:

# Create a public bucket for avatars
curl -X POST "https://api.your-domain.com/storage/v1/bucket" \
  -H "Authorization: Bearer your-service-role-key" \
  -H "Content-Type: application/json" \
  -d '{"id": "avatars", "name": "avatars", "public": true}'

# Create a private bucket for documents
curl -X POST "https://api.your-domain.com/storage/v1/bucket" \
  -H "Authorization: Bearer your-service-role-key" \
  -H "Content-Type: application/json" \
  -d '{"id": "documents", "name": "documents", "public": false}'

Real-Time Subscriptions

Supabase Realtime enables WebSocket subscriptions to database changes:

# Enable replication for a table
# Run in PostgreSQL:
ALTER TABLE public.messages REPLICA IDENTITY FULL;

# Add table to replication publication
ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;

Test real-time with the JavaScript client:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://api.your-domain.com',
  'your-anon-key'
)

// Subscribe to changes
const channel = supabase
  .channel('messages')
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'messages'
  }, (payload) => {
    console.log('Change received:', payload)
  })
  .subscribe()

Edge Functions

Supabase Edge Functions run Deno-based serverless functions:

# Install Supabase CLI
npm install -g supabase

# Initialize project
supabase init

# Create a new function
supabase functions new hello-world

# This creates: supabase/functions/hello-world/index.ts
// supabase/functions/hello-world/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts"

serve(async (req) => {
  const { name } = await req.json()
  return new Response(
    JSON.stringify({ message: `Hello, ${name}!` }),
    { headers: { "Content-Type": "application/json" } }
  )
})
# Deploy function to self-hosted instance
supabase functions deploy hello-world \
  --project-ref your-project-ref \
  --endpoint https://api.your-domain.com

# Test the function
curl -X POST "https://api.your-domain.com/functions/v1/hello-world" \
  -H "Authorization: Bearer your-anon-key" \
  -H "Content-Type: application/json" \
  -d '{"name": "World"}'

Securing and Exposing Your Instance

Nginx Reverse Proxy

# /etc/nginx/sites-available/supabase
server {
    server_name api.your-domain.com;

    location / {
        proxy_pass http://localhost:8000;  # Kong API gateway
        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";
    }
}

server {
    server_name studio.your-domain.com;
    location / {
        proxy_pass http://localhost:3000;  # Supabase Studio
        proxy_set_header Host $host;
    }
}
# Enable site and get SSL
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.your-domain.com -d studio.your-domain.com

Troubleshooting

Services not starting:

# Check logs for specific service
docker compose logs supabase-auth --tail 50
docker compose logs supabase-db --tail 50

# Check for port conflicts
netstat -tlnp | grep -E "8000|5432|3000"

JWT authentication errors:

# Verify JWT_SECRET matches what was used to generate ANON_KEY and SERVICE_ROLE_KEY
# All three must use the same secret
docker compose exec supabase-kong env | grep JWT

Storage upload fails:

# Check storage service logs
docker compose logs supabase-storage --tail 50

# Verify S3 credentials if using S3 backend
docker compose exec supabase-storage env | grep -i aws

Real-time not receiving events:

# Verify table is in publication
docker compose exec supabase-db psql -U postgres -c "\dRp+"

# Restart realtime service
docker compose restart supabase-realtime

Conclusion

Self-hosted Supabase gives you a complete Firebase alternative with PostgreSQL at its core, delivering authentication, real-time, storage, and auto-generated APIs with full data sovereignty. The Docker Compose setup makes it straightforward to run on any Linux VPS, and the Supabase client libraries work identically with self-hosted and cloud instances. Start with a mid-sized VPS and scale individual services as your application grows.