화. 8월 12th, 2025

G: Welcome, fellow developers and DevOps enthusiasts! 👋 Are you enchanted by Supabase’s power but crave more control, perhaps for compliance, cost optimization, or simply the thrill of self-governance? Or maybe you’re looking to brand your API with your own custom domain? You’ve come to the right place!

This comprehensive guide will walk you through the exciting world of self-hosting Supabase and then elevate your setup by configuring a custom API domain. Get ready to unlock new levels of flexibility and professionalism! 🚀


Why Self-Host Supabase? The Power is Yours! 🚀

Supabase, often dubbed “the open-source Firebase alternative,” provides a fantastic hosted service that’s quick to get started with. But, like any cloud service, it has its limits. Self-hosting offers a compelling set of advantages:

  1. Ultimate Control & Customization:

    • Infrastructure Choice: Deploy on your own servers, your preferred cloud provider (AWS, GCP, Azure, DigitalOcean, etc.), or even on-premises. You dictate the hardware, operating system, and network configuration. 🖥️
    • Version Control: Choose specific versions of PostgreSQL, PostgREST, or other Supabase components. Stay on an older stable version or jump to the latest beta.
    • Resource Allocation: Scale up or down your CPU, RAM, and storage precisely as needed, without being bound by predefined tiers.
    • Advanced Configuration: Tweak PostgreSQL settings, PostgREST parameters, or GoTrue configurations beyond what’s typically exposed in the hosted platform.
  2. Data Residency & Compliance:

    • For businesses operating under strict data protection regulations (like GDPR 🇪🇺, HIPAA 🏥, or CCPA 캘리포니아), data residency is paramount. Self-hosting ensures your data remains in a specific geographical location you control, meeting compliance requirements more easily.
    • You have full audit trails and control over who accesses the underlying database server.
  3. Potential Cost Optimization (for large scale):

    • While hosted Supabase is often cost-effective for small to medium projects, self-hosting can become more economical for very large-scale applications or when you can leverage existing infrastructure. You pay for raw compute, not the managed service premium.
    • Caveat: This requires significant DevOps expertise and time investment. Factor in labor costs for setup, maintenance, monitoring, and updates. It’s not always cheaper for smaller projects! 💸
  4. Learning & Experimentation:

    • Deep dive into how Supabase components interact. Understanding the architecture provides invaluable insights for troubleshooting and optimizing your applications.
    • Experiment with new features, custom extensions, or database configurations in a controlled environment. 🔬

The Core Components of a Self-Hosted Supabase Stack 🧱

Before we dive into the “how,” let’s quickly review the major components that make up a Supabase instance. When you self-host, you’re essentially orchestrating these open-source tools to work together:

  • PostgreSQL (Database): The powerful, extensible relational database at the heart of everything. All your data lives here. 💾
  • PostgREST (API Layer): A standalone web server that turns your PostgreSQL database directly into a RESTful API. It generates API endpoints automatically from your database schema. ✨
  • GoTrue (Authentication): A service for managing users, sign-ups, sign-ins, and handling JWTs (JSON Web Tokens) for secure access. 🔑
  • Realtime (WebSockets): A server that allows you to listen to database changes in real-time via WebSockets, enabling live features in your applications. 📡
  • Storage (File Storage): An S3-compatible object storage service that allows you to store and serve files (images, videos, documents) associated with your application. 📦
  • Supabase Studio (Dashboard): A web-based UI that provides a convenient way to manage your database, users, and storage buckets. 📊

The most common way to self-host is using Docker and Docker Compose, which simplifies the deployment and orchestration of these interconnected services.


Getting Started with Self-Hosting: A Step-by-Step Guide 🛠️

Let’s get our hands dirty! For this guide, we’ll assume you have a fresh Linux server or a powerful virtual machine ready to go.

1. Prerequisites:

  • Docker & Docker Compose: Ensure both are installed on your server.

    • Check: docker --version and docker compose version
    • Installation Guide (Ubuntu example):

      # Install Docker
      sudo apt update
      sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
      curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
      echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
      sudo apt update
      sudo apt install docker-ce docker-ce-cli containerd.io -y
      sudo usermod -aG docker $USER # Add your user to the docker group to run without sudo
      newgrp docker # Apply group changes immediately (or logout/login)
      
      # Install Docker Compose (if not already installed with Docker Desktop or as a plugin)
      sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
      sudo chmod +x /usr/local/bin/docker-compose
  • Git: For cloning the Supabase repository.

2. Clone the Supabase Repository:

This repository contains the docker-compose.yml file and example environment variables needed to run Supabase locally.

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

3. Configure Your Environment Variables (.env):

This is crucial for securing and customizing your Supabase instance.

cp .env.example .env

Now, open the .env file with your favorite text editor (nano .env or vim .env):

# .env file snippet (highly abridged)
# You MUST change the SUPABASE_JWT_SECRET to a strong, random 32-character string!
# Use `openssl rand -base64 32` to generate one.
SUPABASE_JWT_SECRET="YOUR_SUPER_SECRET_JWT_SECRET_HERE"

# IMPORTANT: Choose a strong password for your database user
POSTGRES_PASSWORD="your_strong_postgres_password"

# URL for your external network access (we'll update this later for custom domain)
# For now, you can use your server's public IP address or localhost for testing
SUPABASE_URL="http://YOUR_SERVER_IP:8000"
SUPABASE_PUBLIC_URL="http://YOUR_SERVER_IP:8000"

# Other service URLs (typically point to the SUPABASE_URL for client-side use)
# These will be updated later for the custom domain as well.
GOTRUE_URL="http://localhost:9999" # Internal Docker network address for GoTrue
POSTGREST_URL="http://localhost:8000" # Internal Docker network address for PostgREST
REALTIME_URL="http://localhost:4000" # Internal Docker network address for Realtime
STORAGE_URL="http://localhost:5000" # Internal Docker network address for Storage

# CORS Configuration (comma-separated list of allowed origins for your frontend)
# We'll revisit this when setting up your frontend.
SUPABASE_CORS_ORIGINS="*" # For initial testing, but restrict in production!

Key Configuration Points:

  • SUPABASE_JWT_SECRET: ABSOLUTELY CRITICAL! Generate a truly random 32-character base64 string. Do not use the example value. You can generate one with openssl rand -base64 32. This secret is used for signing and verifying JWTs and must be kept secure.
  • POSTGRES_PASSWORD: Set a strong password for your postgres user.
  • SUPABASE_URL / SUPABASE_PUBLIC_URL: For initial testing, you can put your server’s public IP address or localhost. We will change this to your custom domain later.
  • SUPABASE_CORS_ORIGINS: For now, * allows all origins, which is good for initial testing. In production, always restrict this to your actual frontend domain(s) (e.g., https://your-app.com,http://localhost:3000).

4. Start Supabase:

Now, simply bring up your Docker containers!

docker compose up -d
  • up: Starts the services defined in docker-compose.yml.
  • -d: Runs the containers in detached mode (in the background).

This command will download the necessary Docker images and start all Supabase components. It might take a few minutes the first time.

5. Verify Your Setup:

  • Check container status:

    docker ps

    You should see several Supabase-related containers running (PostgreSQL, PostgREST, GoTrue, Realtime, Storage, Studio, InfluxDB, etc.).

  • Check logs for errors:

    docker compose logs
    # or for a specific service, e.g., for GoTrue
    docker compose logs gotrue

    Look for “healthy” messages or errors.

  • Access Supabase Studio: Open your web browser and navigate to http://YOUR_SERVER_IP:3000. You should see the Supabase Studio dashboard. You can log in using the supabase user and the POSTGRES_PASSWORD you set in the .env file.

Congratulations! You now have a self-hosted Supabase instance running. 🎉


Elevate Your Supabase: Why a Custom API Domain? 🌟

Having Supabase running on http://YOUR_SERVER_IP:8000 is functional, but it’s not ideal for a production environment. Here’s why you absolutely should use a custom API domain (e.g., api.yourdomain.com):

  1. Professional Branding & Memorability:

    • api.yourdomain.com looks much more professional and is easier to remember than an IP address or a random subdomain provided by a hosting service. It reinforces your brand identity. 🏢
  2. Enhanced Security (SSL/TLS):

    • A custom domain allows you to easily implement SSL/TLS encryption (HTTPS). This encrypts all traffic between your client applications and your Supabase API, protecting sensitive data from eavesdropping. Browsers will also warn users about unencrypted connections. 🔒
    • Services like Let’s Encrypt provide free, automated SSL certificates, making HTTPS accessible to everyone.
  3. CORS & Browser Compatibility:

    • Modern web browsers enforce Same-Origin Policy. Using a consistent domain for your API (especially with HTTPS) simplifies Cross-Origin Resource Sharing (CORS) configurations, preventing common CORS policy errors when your frontend and backend are on different domains. 🌐
  4. Easier Integration with CDNs & WAFs:

    • If you ever need to put a Content Delivery Network (CDN) in front of your Storage service or a Web Application Firewall (WAF) for enhanced security, a custom domain is a prerequisite.
  5. Future Flexibility:

    • If you ever need to migrate your Supabase instance to a different server or even a different cloud provider, keeping a consistent custom domain means your client applications don’t need to change their API endpoint URL. You just update the DNS record. ➡️

Setting Up Your Custom API Domain: The Technical Deep Dive 🌐

This process involves setting up DNS records, configuring a reverse proxy (like Nginx or Caddy) to handle incoming requests and SSL, and updating your Supabase environment variables.

1. DNS Configuration: Pointing Your Domain to Your Server 🗺️

First, you need to tell the internet that your custom domain (e.g., api.yourdomain.com) points to your server’s public IP address.

  • Go to your domain registrar (e.g., GoDaddy, Namecheap, Cloudflare, etc.) or DNS provider.

  • Create a new A record or CNAME record:

    • Type: A
    • Name/Host: api (or whatever subdomain you prefer, like supabase, backend, etc.)
    • Value/Points To: Your server’s public IP address (e.g., 192.0.2.123)
    • TTL (Time To Live): Keep it low initially (e.g., 300 seconds or 5 minutes) for faster propagation during testing, then increase to 3600 or higher for production.
  • Example DNS Entry:

Type Host Value TTL (seconds)
A api YOUR_SERVER_IP 300
  • Propagation Time: It can take a few minutes to a few hours for DNS changes to propagate across the internet. You can check propagation using tools like dnschecker.org.

2. The Reverse Proxy: Nginx or Caddy? 🔄

A reverse proxy sits in front of your Supabase services, handling incoming requests from the internet, applying SSL, and then forwarding the requests to the correct internal Docker container. We’ll use either Nginx or Caddy.

Why a Reverse Proxy?

  • Single Entry Point: All requests come to https://api.yourdomain.com, and the proxy directs them to the correct internal Supabase service (e.g., PostgREST, GoTrue).
  • SSL Termination: The proxy handles the SSL handshake and certificate management (often with Let’s Encrypt).
  • Load Balancing (Advanced): Can distribute traffic if you scale to multiple Supabase instances.
  • Security: Can filter malicious requests or enforce rate limits.

Option A: Nginx (The Battle-Tested Workhorse) 💪

Nginx is robust and highly configurable. You’ll need to install it and certbot for Let’s Encrypt.

Installation:

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

Nginx Configuration:

Create a new Nginx server block file for your domain (e.g., /etc/nginx/sites-available/supabase.conf):

sudo nano /etc/nginx/sites-available/supabase.conf

Paste the following configuration, replacing api.yourdomain.com with your actual domain and YOUR_SERVER_IP with your server’s internal IP or localhost (if Nginx is on the same machine as Docker).

Important Note: The Supabase Docker containers expose their services on different internal ports. We map them to specific paths in Nginx.

# /etc/nginx/sites-available/supabase.conf

server {
    listen 80;
    server_name api.yourdomain.com; # Replace with your custom domain

    # Redirect all HTTP traffic to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com; # Replace with your custom domain

    # SSL certificates (these will be generated by Certbot)
    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem; # Managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem; # Managed by Certbot
    ssl_trusted_certificate /etc/letsencrypt/live/api.yourdomain.com/chain.pem; # Managed by Certbot

    # Standard SSL configurations
    include /etc/nginx/snippets/ssl-params.conf; # You might need to create this for strong ciphers

    # Define common proxy settings
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_pass_request_headers on;
    proxy_http_version 1.1; # Required for WebSocket (Realtime)

    # Nginx to Docker internal DNS resolution (if you use bridge network and service names)
    # resolver 127.0.0.11; # This resolver points to Docker's internal DNS

    # === Supabase Service Proxying ===

    # GoTrue (Authentication)
    location /auth/ {
        # Note: If GoTrue is directly exposed on 9999, use that.
        # If accessing via Docker Compose internal network:
        # proxy_pass http://gotrue:9999/auth/;
        # If accessing via localhost (if Nginx and Docker are on same host):
        proxy_pass http://localhost:9999/auth/;
        proxy_set_header Authorization $http_authorization; # Pass auth header
    }

    # PostgREST (Database API)
    location /rest/v1/ {
        # proxy_pass http://postgrest:8000/rest/v1/;
        proxy_pass http://localhost:8000/rest/v1/;
    }

    # Realtime (WebSockets for Database Changes)
    location /realtime/v1/ {
        # proxy_pass http://realtime:4000/realtime/v1/;
        proxy_pass http://localhost:4000/realtime/v1/;
        # WebSocket specific headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Storage (File Storage)
    location /storage/v1/ {
        # proxy_pass http://storage:5000/storage/v1/;
        proxy_pass http://localhost:5000/storage/v1/;
    }

    # Supabase Studio (Dashboard - Optional, often kept internal or on a separate subdomain)
    # If you want to expose Studio via Nginx/custom domain:
    location /studio/ {
        # proxy_pass http://studio:3000/;
        proxy_pass http://localhost:3000/;
    }

    # Default fallback for other paths (e.g., health checks or other minor services)
    location / {
        # You might proxy to PostgREST as a default or block unknown paths
        # For simplicity, let's proxy to PostgREST by default, adjust as needed.
        # proxy_pass http://localhost:8000/;
        return 404; # Or better, return a 404 for unknown paths if not intended
    }
}

Activate Nginx Configuration & Get SSL:

  1. Create Symlink:
    sudo ln -s /etc/nginx/sites-available/supabase.conf /etc/nginx/sites-enabled/
  2. Test Nginx config:
    sudo nginx -t

    If you see syntax is ok and test is successful, you’re good.

  3. Reload Nginx:
    sudo systemctl reload nginx
  4. Get Let’s Encrypt SSL Certificate:
    sudo certbot --nginx -d api.yourdomain.com

    Follow the prompts. Certbot will automatically configure Nginx for HTTPS.

  5. Test automatic renewal:
    sudo certbot renew --dry-run

Option B: Caddy (The Simplicity Champion) 👑

Caddy is an excellent alternative that automatically handles HTTPS with Let’s Encrypt, making configuration much simpler.

Installation:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy -y

Caddyfile Configuration:

Edit the default Caddyfile or create a new one (e.g., /etc/caddy/Caddyfile):

sudo nano /etc/caddy/Caddyfile

Replace the content with:

# /etc/caddy/Caddyfile

api.yourdomain.com { # Replace with your custom domain

    # Global options for all services
    handle_errors {
        respond "{err.status_code} {err.status_text}" {
            close
        }
    }

    # GoTrue (Authentication)
    # If GoTrue is directly exposed on 9999, use that.
    # If accessing via Docker Compose internal network (recommended for self-hosting):
    # reverse_proxy /auth/* gotrue:9999
    # If accessing via localhost (if Caddy and Docker are on same host):
    reverse_proxy /auth/* localhost:9999 {
        # Pass headers needed for authentication
        header_up Authorization {http.request.header.Authorization}
    }

    # PostgREST (Database API)
    # reverse_proxy /rest/v1/* postgrest:8000
    reverse_proxy /rest/v1/* localhost:8000

    # Realtime (WebSockets)
    # reverse_proxy /realtime/v1/* realtime:4000
    reverse_proxy /realtime/v1/* localhost:4000 {
        # WebSocket specific headers
        header_up Upgrade {http.request.header.Upgrade}
        header_up Connection {http.request.header.Connection}
    }

    # Storage (File Storage)
    # reverse_proxy /storage/v1/* storage:5000
    reverse_proxy /storage/v1/* localhost:5000

    # Supabase Studio (Dashboard - Optional)
    # reverse_proxy /studio/* studio:3000
    # reverse_proxy /studio/* localhost:3000

    # Fallback for other paths (optional, or remove this and let the services handle 404s)
    # You might want to block access to other paths or return 404
    handle {
        respond "404 Not Found" 404
    }
}

Activate Caddy Configuration:

sudo systemctl enable caddy
sudo systemctl restart caddy
sudo systemctl status caddy # Check status, ensure it's active (running)

Caddy will automatically obtain and renew SSL certificates for api.yourdomain.com when it starts up. How cool is that? 😎


3. Update Supabase Environment Variables (.env):

This is a critical step! Your Supabase containers need to know their external, public-facing URLs. Go back to your supabase/docker/.env file and update the SUPABASE_URL and SUPABASE_PUBLIC_URL to your new custom domain.

# .env file snippet (update these lines)

SUPABASE_URL="https://api.yourdomain.com"
SUPABASE_PUBLIC_URL="https://api.yourdomain.com"

# Also update the individual service URLs if they were pointing to localhost previously
# (they should typically use the custom domain for external access)
GOTRUE_URL="https://api.yourdomain.com/auth"
POSTGREST_URL="https://api.yourdomain.com/rest/v1"
REALTIME_URL="https://api.yourdomain.com/realtime/v1"
STORAGE_URL="https://api.yourdomain.com/storage/v1"

# You might need to add /auth, /rest/v1 etc. to the end of these URLs based on your
# Nginx/Caddy proxy configuration paths if your client SDKs expect the full path.
# Supabase's client SDKs typically append these paths, so ensure your reverse proxy
# is correctly configured to match.

After updating, you must restart your Supabase Docker containers for the changes to take effect:

cd supabase/docker
docker compose down
docker compose up -d

4. CORS Configuration Revisited:

With your custom domain, make sure your SUPABASE_CORS_ORIGINS in .env is correctly configured to allow requests from your frontend application’s domain.

# .env file snippet
SUPABASE_CORS_ORIGINS="https://your-frontend-app.com,http://localhost:3000"
# If you have multiple frontends, separate them with commas.
# Do NOT use "*" in production unless you know exactly what you are doing.

Remember to restart Supabase after changing CORS settings.


Common Pitfalls & Troubleshooting Tips 🚨

Self-hosting can be tricky, but most issues are solvable. Here are some common problems and how to tackle them:

  • DNS Propagation Delay: If ping api.yourdomain.com doesn’t show your server’s IP immediately after setting DNS, wait a bit longer. Use dnschecker.org.
  • Firewall Issues: Your server’s firewall (e.g., ufw on Ubuntu, cloud provider security groups) must allow incoming traffic on ports 80 (for initial Certbot challenge) and 443 (for HTTPS).
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw status verbose
  • SSL Certificate Problems:
    • Ensure your Nginx/Caddy config correctly points to the .pem files generated by Certbot.
    • Check Certbot logs (/var/log/letsencrypt/).
    • Make sure port 80 is open for the http-01 challenge.
  • Environment Variable Mismatch: This is the most common cause of client-side errors. Double-check SUPABASE_URL, SUPABASE_PUBLIC_URL, and the individual service URLs in your .env file match your custom domain.
  • Nginx/Caddy Config Errors:
    • For Nginx, always run sudo nginx -t before reloading.
    • Check Nginx logs (/var/log/nginx/error.log).
    • For Caddy, check sudo journalctl -u caddy.
  • Docker Container Issues:
    • Check docker ps to ensure all containers are Up.
    • Use docker compose logs [service_name] (e.g., docker compose logs postgrest) to view specific service logs for errors.
    • Ensure there are no port conflicts.
  • Client SDK Configuration: Remember to update your frontend application’s Supabase client initialization to use your new custom domain:

    import { createClient } from '@supabase/supabase-js'
    
    const supabaseUrl = 'https://api.yourdomain.com' // YOUR CUSTOM DOMAIN HERE
    const supabaseKey = 'YOUR_SUPABASE_ANON_KEY' // Get this from Supabase Studio dashboard > Project Settings > API
    const supabase = createClient(supabaseUrl, supabaseKey)
  • Anon Key: The SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY are generated automatically by Supabase when it starts. You can find these in the Supabase Studio dashboard once logged in (Project Settings -> API).

Conclusion ✨

You’ve done it! You’ve navigated the complexities of self-hosting Supabase and, more importantly, branded it with your very own custom API domain. This setup not only gives you unparalleled control over your data and infrastructure but also presents a polished, professional face to your applications.

While self-hosting comes with the responsibility of maintenance and security, the benefits of flexibility, compliance, and customizability are immense. With your custom domain, your Supabase API is now a first-class citizen in your infrastructure, ready to power your most ambitious projects securely and efficiently.

Happy coding, and may your APIs always be fast and your data always be secure! 🚀🔐

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다