Skip to Content

Own Your Backend: How to Self-Host Supabase with Docker from Scratch

Follow this complete guide to self-host Supabase with Docker on your own server — full Postgres, Auth, Storage, and Studio, with zero vendor lock-in.

Why Self-Host Supabase?

Supabase has become one of the fastest-growing backend platforms on the planet — and for good reason. It ships Postgres, Auth, Realtime, Storage, and Edge Functions as a single integrated stack. Supabase recently doubled its valuation to $10B, which tells you exactly how much momentum is behind the project right now.

The managed cloud is excellent. But there are real reasons to run your own instance:

  • Data residency: Your Postgres data stays in your jurisdiction, your data centre, your rules.
  • Cost at scale: Cloud pricing scales with rows, bandwidth, and users. A VPS doesn't.
  • Compliance: HIPAA, GDPR, SOC 2 — easier to demonstrate when the hardware is yours.
  • Customisation: Postgres extensions, custom auth flows, tweaked docker-compose configs — it's all open.

This guide walks you through the full self-host Supabase Docker setup: from a blank server to a production-hardened instance with HTTPS, secrets locked down, and Storage working. We also have a companion deep-dive — Self-Host Supabase Docker: Run Your Own Firebase Alternative With Full Control — which covers advanced customisation if you want to go further after this guide.


Prerequisites

Get these in place before you start. The setup will fail in hard-to-diagnose ways if any of these are missing.

Server Requirements

Resource Minimum Recommended
RAM4 GB8 GB+
CPU2 cores4 cores+
Disk40 GB SSD80 GB+ SSD

A $20/month VPS (2 vCPU, 4 GB RAM) handles small-to-medium workloads fine. Scale the disk generously — Postgres and Storage objects live there.

Software

  • Ubuntu 22.04 or 24.04 (this guide uses Ubuntu)
  • Docker Engine 24+ with Docker Compose v2
  • Git
  • A domain name with an A record pointing to your server (required for HTTPS)
  • Port 80 and 443 open in your firewall; port 8000 for the API gateway

Verify Your Stack

docker --version          # Docker version 24+
docker compose version    # Docker Compose version v2+
git --version

Step 1 — Clone the Supabase Repository

Supabase ships its entire Docker configuration inside its main monorepo. You only need the docker/ subdirectory.

Option A: Quick Start Script (Linux Only)

If you're on a supported Linux distro (Debian/Ubuntu or RHEL/CentOS/Fedora), the official setup script handles everything — Docker install, sparse clone, secret generation, and URL prompts:

curl -fsSL https://supabase.link/setup.sh | sh

The script generates all JWT secrets, API keys, and a random dashboard password automatically, then prompts you for your domain URLs. If you go this route, skip to Step 3.

Option B: Manual Clone (All Platforms)

For more control — or on macOS/Windows — clone manually:

# Clone only the docker/ directory (sparse clone is faster)
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/docker

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

Step 2 — Configure Secrets and Environment Variables

This is the step most guides rush. Every secret in the .env file defaults to an example value that is publicly known. You must change them before starting the stack — not after, not "later for production". Now.

Generate Secure Values

You need a JWT_SECRET (at least 32 characters, random), an ANON_KEY, and a SERVICE_ROLE_KEY. The anon and service role keys are JWTs signed with your JWT_SECRET.

# Generate a strong JWT secret
openssl rand -base64 48

# You can also use the Supabase JWT generator at:
# https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys
# Paste your JWT_SECRET there to get pre-signed ANON_KEY and SERVICE_ROLE_KEY values

Edit the .env File

Open .env and update all the mandatory secrets. The key fields are:

############
# Secrets — CHANGE EVERY ONE OF THESE
############
POSTGRES_PASSWORD=your-strong-postgres-password
JWT_SECRET=your-long-random-jwt-secret-min-32-chars
ANON_KEY=eyJhb...   # paste the generated anon JWT here
SERVICE_ROLE_KEY=eyJhb...  # paste the generated service role JWT here
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=your-strong-dashboard-password

############
# API URLs — update to match your domain
############
SITE_URL=https://app.yourdomain.com
API_EXTERNAL_URL=https://api.yourdomain.com
SUPABASE_PUBLIC_URL=https://api.yourdomain.com

############
# SMTP — required for auth emails
############
[email protected]
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USER=your-smtp-username
SMTP_PASS=your-smtp-password
SMTP_SENDER_NAME=Your App Name

Critical: ANON_KEY and SERVICE_ROLE_KEY must be JWTs signed with the exact JWT_SECRET you set. If these three values don't match, every API call will return 401s. Use the Supabase key generator to produce them correctly.

Postgres Configuration (Optional Tuning)

For production workloads, tune Postgres memory settings. Edit or create volumes/db/postgresql.conf overrides:

# volumes/db/postgresql.conf additions (tune to your server's RAM)
shared_buffers = 256MB          # ~25% of total RAM
effective_cache_size = 768MB    # ~75% of total RAM
work_mem = 16MB
maintenance_work_mem = 64MB
max_connections = 100

Step 3 — Start the Stack

With secrets configured, start all services:

# Pull images first (saves time — images are large)
docker compose pull

# Start all services detached
docker compose up -d

# Watch the logs to confirm everything comes up healthy
docker compose logs -f

The first boot takes 2–3 minutes as Postgres initialises and migrations run. You'll know it's ready when supabase-studio logs show it's listening on port 3000.

Check Service Health

# All containers should show "Up" or "Up (healthy)"
docker compose ps

# Hit the API gateway health endpoint
curl http://localhost:8000/rest/v1/ \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

A 200 OK response confirms PostgREST is live and your JWT keys are correctly wired up.

Access Studio

By default, Supabase Studio runs on port 3000. Navigate to http://your-server-ip:3000 and log in with the DASHBOARD_USERNAME and DASHBOARD_PASSWORD you set in .env.


Step 4 — HTTPS with Nginx and Let's Encrypt

Plain HTTP is fine for local development. For any public-facing instance — especially if you're using OAuth providers or sending auth emails — HTTPS is required. Supabase's own docs recommend placing Nginx or Caddy in front of the Kong API gateway.

Install Nginx and Certbot

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Nginx Site Config

Create /etc/nginx/sites-available/supabase. This proxies all traffic to Kong (port 8000) and Studio (port 3000) under separate subdomains — replace the domains with your own:

# API gateway
server {
    listen 80;
    server_name api.yourdomain.com;

    location / {
        proxy_pass         http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
        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_read_timeout 300s;
        client_max_body_size 50M;
    }
}

# Studio dashboard
server {
    listen 80;
    server_name studio.yourdomain.com;

    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-Proto $scheme;
    }
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# Issue certificates for both subdomains
sudo certbot --nginx -d api.yourdomain.com -d studio.yourdomain.com

Once Certbot runs, update API_EXTERNAL_URL, SUPABASE_PUBLIC_URL, and SITE_URL in your .env to use the https:// URLs, then restart the stack:

docker compose down
docker compose up -d

Step 5 — Post-Install Configuration

The stack is running and secured. Here's what to do next to make it production-ready.

Edge Functions

Edge Functions live in volumes/functions/. Create a subdirectory per function:

mkdir -p volumes/functions/hello-world
cat > volumes/functions/hello-world/index.ts << 'EOF'
Deno.serve(async (req) => {
  return new Response(
    JSON.stringify({ message: "Hello from self-hosted Edge Functions!" }),
    { headers: { "Content-Type": "application/json" } }
  )
})
EOF

# Restart the functions service to pick up the new function
docker compose restart functions

Invoke it via the API gateway: POST https://api.yourdomain.com/functions/v1/hello-world

Row Level Security

Always enable RLS on tables that users can access. Without it, your anon key is effectively a root key to the entire table. In Studio, go to Table Editor → Your Table → RLS and enable it, then create policies for SELECT, INSERT, UPDATE, and DELETE.

Postgres Backups

Data in volumes/db/data is in a Docker volume. For automated backups, use pg_dump on a cron schedule:

# Run a dump from inside the Postgres container
docker exec supabase-db pg_dump \
  -U postgres \
  -d postgres \
  --no-password \
  | gzip > /backups/supabase-$(date +%Y%m%d-%H%M%S).sql.gz

# Add to crontab for nightly backups at 2am
# 0 2 * * * /path/to/backup-script.sh

Keeping Supabase Updated

Pull the latest images and restart. Your data is in named volumes — it survives image updates:

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

Step 6 — Troubleshooting Common Issues

These are the problems you'll actually encounter. Most have fast fixes once you know what to look for.

Problem: All API Calls Return 401

Your ANON_KEY or SERVICE_ROLE_KEY does not match the JWT_SECRET. This is the single most common self-hosting mistake.

Fix: Go to the Supabase key generator, paste your JWT_SECRET, copy the two generated keys, update .env, and restart the stack. The keys must be re-generated — you cannot manually edit a JWT and keep it valid.

Problem: Studio Shows "Unable to Connect"

Studio can't reach the API gateway. Check that Kong is running and healthy:

docker compose ps supabase-kong
docker compose logs supabase-kong --tail 50

# Manually verify the gateway is reachable on the host
curl -I http://localhost:8000

If Kong fails to start, it's usually a bad ANON_KEY or SERVICE_ROLE_KEY format — see above.

Problem: Auth Emails Not Sending

Check your SMTP_* variables in .env. Most cloud providers (AWS, GCP, DigitalOcean) block outbound port 25 by default — use port 587 or 465 with an SMTP relay (Resend, Postmark, SendGrid). Check the GoTrue auth container logs for SMTP errors:

docker compose logs supabase-auth --tail 100 | grep -i smtp

Problem: Realtime Connections Failing

Realtime validates JWTs on every WebSocket connection. If the JWT_SECRET is inconsistent across services, Realtime will reject connections silently. Confirm all services share the same secret:

grep JWT_SECRET .env
docker compose logs supabase-realtime --tail 50

Also confirm your Nginx config includes proxy_read_timeout 86400; for the /realtime/ location — WebSocket connections are long-lived and will be dropped without it.

Problem: Postgres Container Crashes on Restart

Usually a permissions issue on the volume mount. Fix with:

sudo chown -R 999:999 volumes/db/data
docker compose up -d supabase-db

Tip: Don't Expose Postgres Directly to the Internet

Port 5432 should never be open to the public. If you need direct Postgres access from a remote machine, use an SSH tunnel:

# Tunnel remote Postgres to local port 5433
ssh -L 5433:localhost:5432 user@your-server-ip -N

Then connect your client to localhost:5433. No firewall rule needed, no Postgres exposed.

Tip: Disable Sign-Ups for Private Deployments

If this is an internal app, you don't want open registration. In Studio, go to Authentication → Settings → Disable Sign-ups and toggle it off. You can still create users manually from the dashboard or via the service role key.


What You've Got

At this point your self-hosted Supabase setup gives you:

  • A fully managed Postgres database with logical replication enabled
  • GoTrue Auth — email/password, magic links, OAuth, phone OTP
  • PostgREST auto-generating REST APIs from your schema
  • Realtime for live subscriptions via WebSockets
  • Supabase Storage for file uploads with S3-compatible access
  • Edge Functions running Deno serverless handlers
  • Studio — the full dashboard UI, locally hosted
  • HTTPS via Nginx with auto-renewing Let's Encrypt certificates

That's the entire Firebase-alternative stack, running on a server you control. Want to go deeper on customising auth flows, adding Logflare analytics, or wiring up your own S3-compatible object store? Check out our companion guide: Self-Host Supabase Docker: Run Your Own Firebase Alternative With Full Control.


Need Production-Grade Infrastructure?

A single-server Docker setup is a great starting point. When you're ready to add high availability, automated backups to object storage, monitoring, zero-downtime deployments, or security hardening for compliance — that's where the architecture gets more complex.

The Sysbrix team builds and maintains production backend infrastructure for teams who want the benefits of self-hosting without the ops overhead. If you're evaluating whether self-hosting Supabase makes sense for your team at scale, we're happy to talk it through.

Talk to Us About Enterprise Supabase Deployments →

Run Your Own ChatGPT: The Complete Open WebUI Setup Guide
Deploy a private, self-hosted AI chat interface on your own server in under 30 minutes — no data leaving your control.