Skip to main content

Self-Host Supabase 2026: Docker Setup Guide

·OSSAlt Team
supabasebackendself-hostingdockerguide
Share:

Supabase is the open source Firebase alternative — PostgreSQL database, authentication, storage, edge functions, and real-time subscriptions. Self-hosting removes the 500 MB database limit, gives you unlimited API requests, and costs a fraction of Supabase Cloud's $25/month Pro plan.

Why Self-Host Supabase

Supabase Cloud's free tier is surprisingly generous — 500 MB database, 5 GB storage, 50K monthly active users. But it has a critical limitation: projects on the free tier pause after 7 days of inactivity. For production apps with occasional traffic, that's disqualifying.

The Pro plan at $25/month removes pausing and gives 8 GB database storage. But 8 GB fills up faster than you'd expect for any data-rich application — at that point you're paying for additional database space on top of the $25 base. At $0.125/GB-month, a 50 GB database adds another $6.25/month. Organizations need to run the numbers carefully as they scale.

Self-hosting Supabase on a Hetzner CX32 (4 vCPU, 8 GB RAM, €8/month) gives you essentially unlimited PostgreSQL storage constrained only by disk size, all Supabase services included, and no per-project fees. For a SaaS app with a 50 GB database, self-hosting saves roughly $300/year compared to Supabase Cloud's Pro tier.

Data ownership and compliance: Some industries require that data never leave specific jurisdictions. Supabase Cloud runs on AWS; self-hosting lets you deploy in any Hetzner, Scaleway, or OVH datacenter in your required region. HIPAA, GDPR, and other compliance frameworks are significantly simpler when you control the infrastructure.

No vendor lock-in: Since Supabase is just PostgreSQL under the hood, self-hosted data is always exportable. Your schemas, rows, and RLS policies are standard SQL — there's no proprietary format to escape if you decide to migrate.

When NOT to self-host Supabase: Supabase's Docker setup runs 8+ services (Kong, GoTrue, PostgREST, Realtime, Storage, Studio, PostgreSQL, Meta). This is significant operational complexity. If you're a solo developer building a side project, the managed Supabase Pro plan or Firebase is a better tradeoff. Self-hosting makes most sense for teams with existing DevOps capacity or companies where the cost savings justify the operational overhead.

Prerequisites

Supabase is the most resource-intensive tool in this guide due to its multi-service architecture. Choosing the right VPS provider matters more here than for single-service deployments.

Server specs: Supabase's official recommendation is 4 GB RAM minimum, 8 GB recommended. With 8+ Docker services running, each consuming 100-300 MB, a 4 GB server is tight. For a production app with real users, start with 8 GB RAM. The PostgreSQL service is the largest consumer — allocate roughly 25% of available RAM to the database via the shared_buffers configuration.

Disk space: PostgreSQL data accumulates quickly. Start with at least 50 GB and use a separate block storage volume that you can expand without resizing the root disk. Hetzner's volumes are €0.0400/GB-month and can be attached and resized online.

Operating system: Ubuntu 22.04 LTS is recommended. Supabase's official Docker documentation targets Ubuntu, and the Hetzner Marketplace image is pre-configured for Docker on Ubuntu 22.04.

Docker and Docker Compose v2: The Supabase docker directory uses docker compose (not docker-compose). Install Docker Engine with curl -fsSL https://get.docker.com | sh and verify docker compose version shows 2.x.

JWT key generation: Supabase requires specific JWT tokens generated from your JWT secret. The official docs provide a generator at supabase.com/docs/guides/self-hosting/docker#generate-api-keys — use it rather than generating random strings. The anon key and service_role key are JWTs with specific claims Supabase expects.

Skills required: Intermediate Linux and Docker knowledge. You should be comfortable troubleshooting container logs, understanding multi-service Docker networking, and configuring nginx/Caddy for multiple upstream services.

Requirements

  • VPS with 4 GB RAM minimum (8 GB recommended)
  • Docker and Docker Compose
  • Domain name (e.g., supabase.yourdomain.com)
  • 30+ GB disk

Step 1: Clone Supabase Docker

git clone --depth 1 https://github.com/supabase/supabase.git
cd supabase/docker

# Copy environment
cp .env.example .env

Step 2: Generate Secrets

# Generate JWT secret
openssl rand -hex 32

# Generate anon key and service role key using the JWT secret
# Use https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys
# Or generate manually with jwt.io

Step 3: Configure Environment

Edit .env:

# General
SITE_URL=https://supabase.yourdomain.com
API_EXTERNAL_URL=https://supabase.yourdomain.com

# JWT
JWT_SECRET=your-jwt-secret-min-32-chars
ANON_KEY=your-generated-anon-key
SERVICE_ROLE_KEY=your-generated-service-role-key

# Database
POSTGRES_PASSWORD=your-strong-password
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432

# Dashboard
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=your-dashboard-password

# SMTP
SMTP_HOST=smtp.resend.com
SMTP_PORT=587
SMTP_USER=resend
SMTP_PASS=re_your_api_key
SMTP_SENDER_NAME=Supabase
SMTP_ADMIN_EMAIL=admin@yourdomain.com

# Storage
STORAGE_BACKEND=file
FILE_SIZE_LIMIT=52428800

Step 4: Start Supabase

docker compose up -d

This starts all Supabase services:

ServicePortPurpose
Kong (API Gateway)8000API routing
GoTrue9999Authentication
PostgREST3000REST API from PostgreSQL
Realtime4000WebSocket subscriptions
Storage5000File storage API
Studio3001Dashboard UI
PostgreSQL5432Database
Meta8080Metadata API

Step 5: Reverse Proxy (Caddy)

# /etc/caddy/Caddyfile
supabase.yourdomain.com {
    # API Gateway
    reverse_proxy localhost:8000
}

studio.yourdomain.com {
    # Dashboard
    reverse_proxy localhost:3001
}
sudo systemctl restart caddy

Step 6: Access Dashboard

  1. Open https://studio.yourdomain.com
  2. Login with your dashboard credentials
  3. Create your first project

Step 7: Connect Your App

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

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

// Query data
const { data, error } = await supabase
  .from('todos')
  .select('*')

// Insert data
const { data, error } = await supabase
  .from('todos')
  .insert({ title: 'My todo', completed: false })

// Auth
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123',
})

// Storage
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('user1/avatar.png', file)

// Realtime
supabase
  .channel('todos')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'todos' },
    (payload) => console.log('Change:', payload)
  )
  .subscribe()

See also: Best SaaS Boilerplates with Supabase on StarterPick — once self-hosted, use a boilerplate to build on top.

Step 8: Set Up Row Level Security

Enable RLS on your tables for production security:

-- Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

-- Users can only read their own todos
CREATE POLICY "Users read own todos"
ON todos FOR SELECT
USING (auth.uid() = user_id);

-- Users can only insert their own todos
CREATE POLICY "Users insert own todos"
ON todos FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can only update their own todos
CREATE POLICY "Users update own todos"
ON todos FOR UPDATE
USING (auth.uid() = user_id);

Production Hardening

Restrict Studio access:

  • Put Studio behind VPN or IP allowlist
  • Use strong dashboard credentials
  • Consider disabling Studio in production

Backups:

# Database backup (daily cron)
docker exec supabase-db pg_dump -U postgres postgres > /backups/supabase-$(date +%Y%m%d).sql

# Storage backup
tar czf /backups/supabase-storage-$(date +%Y%m%d).tar.gz ./volumes/storage

For reliable offsite backups, combine daily pg_dump with automated server backups with restic to push encrypted snapshots to Backblaze B2. Supabase storage files (user uploads) are on disk alongside the database — back them up together.

Updates:

cd supabase/docker
git pull
docker compose pull
docker compose up -d

Monitoring:

  • Monitor Kong API gateway (port 8000)
  • Monitor PostgreSQL connections and query performance
  • Set up disk space alerts (database grows)
  • Monitor GoTrue for auth failures

Resource Usage

ScaleRAMCPUDisk
Development4 GB2 cores20 GB
Small app (1K users)8 GB4 cores50 GB
Medium app (10K users)16 GB8 cores100 GB

VPS Recommendations

ProviderSpec (small app)Price
Hetzner4 vCPU, 8 GB RAM€8/month
DigitalOcean4 vCPU, 8 GB RAM$48/month
Linode4 vCPU, 8 GB RAM$48/month

Supabase is resource-intensive due to multiple services. Use Hetzner for the best price-performance ratio.

Production Security Hardening

Supabase exposes a large attack surface — PostgreSQL, an HTTP API gateway, authentication endpoints, file storage, and a dashboard UI. Locking this down properly is critical. Follow the full self-hosting security checklist and apply these Supabase-specific hardening steps.

UFW firewall: External traffic should only reach Caddy. Block direct access to all Supabase service ports.

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
# Block direct service access — use Caddy as the only entry point
sudo ufw deny 8000/tcp   # Kong
sudo ufw deny 3001/tcp   # Studio
sudo ufw deny 5432/tcp   # PostgreSQL
sudo ufw enable

Fail2ban for SSH:

sudo apt install fail2ban -y

/etc/fail2ban/jail.local:

[sshd]
enabled = true
maxretry = 5
bantime = 3600
findtime = 600

Secrets management: Your .env file contains JWT secrets and service role keys that provide full admin access to your database. These are more sensitive than a typical app password.

chmod 600 .env
echo ".env" >> .gitignore
# Back up the secrets separately — losing them means rebuilding auth

Disable SSH password auth: Edit /etc/ssh/sshd_config:

PasswordAuthentication no
PermitRootLogin no

Restart: sudo systemctl restart sshd

Automatic security updates:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Studio access restriction: The Supabase Studio dashboard (studio.yourdomain.com) should not be publicly accessible. Options: put it behind a VPN, restrict by IP using Caddy's remote_ip matcher, or add HTTP basic auth as an additional layer. The Studio has direct database access — it's the highest-privilege surface.

Service role key handling: Never expose the SERVICE_ROLE_KEY to client-side code. It bypasses Row Level Security entirely. Only use it in server-side code (API routes, server functions) where it's not visible to end users.

Troubleshooting Common Issues

Kong returns 401 Unauthorized on all API requests

The most common cause is a mismatch between the JWT secret used to generate the anon/service_role keys and the JWT_SECRET in your .env. Regenerate the anon key and service_role key using the official Supabase key generator with your exact JWT secret value. Copy the tokens precisely — even a single character difference causes this error.

GoTrue authentication emails not sending

Check docker logs supabase-auth for SMTP errors. Verify your SMTP credentials with a test send using swaks or your email provider's dashboard. Also confirm the SITE_URL matches your actual deployment URL — GoTrue uses it to construct email confirmation links.

PostgREST returns "Not Found" for tables

PostgREST only exposes tables that exist in the public schema by default. If you've created tables in a different schema, they won't appear automatically. Also verify that RLS is configured correctly — if RLS is enabled without any policies, the table returns empty results for anon/authenticated roles even if rows exist.

Realtime subscriptions disconnect frequently

This is usually a memory issue. Realtime holds WebSocket connections in memory; if the container is approaching its memory limit, it kills connections to free resources. Check docker stats supabase-realtime and consider scaling up RAM or limiting the number of simultaneous subscriptions per client.

Storage uploads fail with 413 or timeout

Caddy has a default request body limit. For Supabase storage, you need to raise it:

supabase.yourdomain.com {
    reverse_proxy localhost:8000
    request_body {
        max_size 500MB
    }
}

Also check FILE_SIZE_LIMIT in your .env — the default 50 MB may be too small for your use case.

Database disk fills up unexpectedly

PostgreSQL's WAL (write-ahead log) can consume significant disk space during heavy write workloads. Monitor with: docker exec supabase-db du -sh /var/lib/postgresql/data/pg_wal. If WAL is growing unbounded, check for long-running transactions or replication slots. In your Caddy + Supabase setup, WAL archiving is disabled by default — run VACUUM regularly and set wal_keep_size appropriately.

Ongoing Maintenance and Operations

Supabase's multi-service architecture means more moving parts to monitor, but once stable, it runs for weeks without requiring attention. Here's what long-term operations look like.

Service health monitoring. With 8+ services running, you want a health check that verifies the full stack, not just one port. Supabase's Kong gateway at port 8000 proxies all services — if Kong is up but GoTrue is down, auth requests will silently fail. Set up endpoint monitors for the specific API endpoints your application uses: https://supabase.yourdomain.com/auth/v1/health, https://supabase.yourdomain.com/rest/v1/, and https://supabase.yourdomain.com/storage/v1/status.

PostgreSQL performance tuning. The default PostgreSQL configuration in Supabase's Docker setup is conservative. For production apps with meaningful traffic, tune these settings in your PostgreSQL config or environment: shared_buffers to 25% of available RAM, effective_cache_size to 75% of RAM, and max_connections to 100 (with connection pooling via PgBouncer if needed). These changes require restarting the db container.

Keeping the Supabase stack updated. Run git pull && docker compose pull && docker compose up -d monthly to stay current on security patches. Supabase's services (GoTrue, PostgREST, Realtime) release updates independently. Check the Supabase self-hosting changelog on GitHub before major updates — occasionally a service update requires a new environment variable or migration step.

Edge functions and serverless. Self-hosted Supabase supports edge functions via the supabase/edge-runtime image. If you're using edge functions in your app, you'll need to add the edge-runtime service to your Docker Compose and deploy function bundles manually (unlike Supabase Cloud, which handles deployments automatically). This is an advanced setup documented in the Supabase self-hosting docs.

Storage scaling. The default STORAGE_BACKEND=file stores all uploads on the local filesystem. For any production app with user-uploaded content, this creates a backup complexity problem — you need to back up both the database and the file storage volume together (they must be consistent). Switch to S3-compatible storage (STORAGE_BACKEND=s3) before you have significant upload data; migrating existing files later is painful. Cloudflare R2 offers zero-egress-fee S3-compatible storage that works well with Supabase.

RLS policy auditing. Row Level Security policies are your primary defense against data exposure. Periodically audit your RLS policies to ensure no tables have been accidentally left with permissive rules. Query the PostgreSQL system catalog: SELECT tablename, policies FROM pg_tables LEFT JOIN pg_policies ON pg_tables.tablename = pg_policies.tablename WHERE schemaname = 'public';. Any table with rowsecurity = false on a public schema table is accessible without restriction.

Using Supabase for multiple projects. A single self-hosted Supabase instance can serve multiple applications by using separate PostgreSQL schemas or separate databases. The Studio dashboard supports multiple projects on a single installation. This approach is cost-effective for small teams running several projects — instead of paying $25/month per project on Supabase Cloud, one self-hosted instance covers everything. Configure each project with its own JWT keys and service role keys for full isolation, even though they share the same underlying PostgreSQL server. This multi-tenant approach works well for development and staging environments where strict isolation is less critical.


Compare backend platforms on OSSAlt — features, pricing, and self-hosting options side by side.

See open source alternatives to Supabase on OSSAlt.

The SaaS-to-Self-Hosted Migration Guide (Free PDF)

Step-by-step: infrastructure setup, data migration, backups, and security for 15+ common SaaS replacements. Used by 300+ developers.

Join 300+ self-hosters. Unsubscribe in one click.