Self-Host Supabase on Ubuntu Server
Deploy a full Supabase stack on your own Ubuntu server using Docker.
Introduction
30 Second Summary
When your apps depend on a managed database service, you're trusting someone else with your data. Self-hosting gives you full control over your infrastructure and removes that dependency entirely.
In this project, you will deploy a complete Supabase instance on your own Ubuntu server using Docker Compose, migrate your existing database, and connect your Next.js apps to the self-hosted instance.
What You'll Build
A production-ready self-hosted Supabase deployment on your Ubuntu rack server, complete with HTTPS and automated TLS certificates via a Caddy reverse proxy.
By the end of this project, you'll have:
- Self-hosted Supabase running via Docker Compose with PostgreSQL, Auth, Realtime, Storage, and Studio dashboard.
- Migrated database with your existing schema and data transferred from managed Supabase to your self-hosted instance.
- Updated Vercel environment variables pointing both Next.js apps at your self-hosted instance with zero code changes.
- Secret Mission: Automated daily database backups using a cron job with retention management.
Are there any prerequisites?
You need SSH access to an Ubuntu server (with at least 4 GB RAM, 2 CPU cores, and 40 GB disk space), an existing managed Supabase project with data, and Next.js apps deployed on Vercel.
You also need DNS control for your domain so you can point a subdomain at your server.
Set Up Docker on Your Ubuntu Server
Your self-hosted Supabase stack runs entirely inside Docker containers. Before you can deploy anything, you need a working Docker environment on your Ubuntu server.
In this step, you will SSH into your server, confirm it has enough resources, and install Docker Engine along with Docker Compose.
In this step, get ready to:
- Verify your server meets hardware requirements.
- Update Ubuntu and install prerequisite packages.
- Install Docker Engine and Docker Compose.
SSH into the server and verify hardware
Supabase self-hosted requires a minimum of 4 GB RAM, 2 CPU cores, and 40 GB disk space. You need to confirm your server meets these requirements before proceeding.
- Open your SSH client on your Windows PC. Press the Windows key to open your search bar.
- Type Terminal and press Enter to open Windows Terminal.
- Connect to your Ubuntu server by running this command:
ssh [[SSH_USER="your-username"]]@[[SERVER_IP="your-server-ip-address"]]
- Once connected, check your available RAM by running:
free -h
- Confirm the total column under Mem shows at least 4 GB (e.g. 7.7Gi or higher).
- Check your CPU core count by running:
nproc
- Confirm the output shows 2 or higher.
- Check available disk space by running:
df -h
- Look at the row where Mounted on is /. Confirm the Avail column shows at least 40 GB.
What do these commands check?
free -h displays your system memory in a human-readable format. nproc prints the number of processing units available. df -h shows the disk space usage for all mounted filesystems.
Server doesn't meet requirements?
If your server shows less than 4 GB RAM or fewer than 2 cores, the Supabase stack may fail to start or run unreliably. You would need to upgrade your hardware or use a different machine.
help me figure out if my server can run self-hosted Supabase
Update Ubuntu and install prerequisites
Before installing Docker, you need to make sure your system packages are up to date and that a few tools Supabase depends on are available.
- Check your Ubuntu version by running:
lsb_release -a
- You should see your Ubuntu version listed (e.g. 22.04 or 24.04).
- Update your package lists and upgrade installed packages by running:
sudo apt update && sudo apt upgrade -y
What does this command do?
sudo apt update refreshes the list of available packages from all repositories. sudo apt upgrade -y installs newer versions of all currently installed packages without asking for confirmation.
- Install the prerequisite tools that Supabase and Docker depend on by running:
sudo apt install -y git openssl jq curl
Why these four packages?
git is used by the Supabase setup script to clone configuration files. openssl generates cryptographic keys for securing your instance. jq processes JSON data in configuration scripts. curl downloads files and scripts from the internet.
- Confirm the tools installed successfully by running:
git --version && openssl version && jq --version && curl --version | head -1
- You should see version numbers printed for each tool with no errors.
Package installation failed?
If apt install reports errors about locked files, another process may be using the package manager. Wait a minute and try again.
If packages are not found, your apt update may not have completed successfully. Run sudo apt update again and check for network errors.
help me troubleshoot apt package installation errors on Ubuntu
Install Docker Engine and Docker Compose
Docker Engine runs containers on your server. Docker Compose orchestrates multi-container applications like the Supabase stack. You will install both from Docker's official apt repository.
- Set up Docker's official apt repository by running this block of commands:
# Add Docker's official GPG key
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the Docker repository to apt sources
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
What does this do?
This downloads Docker's GPG signing key so your system can verify the authenticity of Docker packages. It then adds Docker's official package repository to your system so apt knows where to find Docker packages.
- Install Docker Engine, the CLI, and the Compose plugin by running:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What are these packages?
docker-ce is the Docker Engine daemon. docker-ce-cli is the command-line client you interact with. containerd.io manages container lifecycles. docker-buildx-plugin handles image building. docker-compose-plugin provides the docker compose command for orchestrating multi-container stacks.
- When prompted to continue, type Y and press Enter.
- Verify Docker Engine is installed by running:
docker --version
- You should see output showing a Docker version number (e.g. Docker version 29.x.x).
- Verify Docker Compose is installed by running:
docker compose version
- You should see output showing a Docker Compose version number (e.g. Docker Compose version v5.x.x).
✔️ I see version numbers for both
Docker Engine and Docker Compose are ready. You are all set for the next step.
ⓧ I see 'command not found' or an error
If docker --version returns an error, Docker Engine did not install correctly.
- Check that the repository setup completed without errors. Re-run sudo apt update and look for error messages about the Docker repository.
- Make sure you are running a supported Ubuntu version (22.04 or 24.04 LTS). Check with lsb_release -a.
- Re-run the install command:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
If docker compose version fails but docker --version works, the Compose plugin may not have installed. Re-run:
sudo apt install docker-compose-plugin
help me troubleshoot Docker installation on Ubuntu
- Add your current user to the docker group so you can run Docker commands without sudo:
sudo usermod -aG docker $USER
- Apply the group change by running:
newgrp docker
Why add yourself to the docker group?
By default, Docker commands require sudo because the Docker daemon runs as root. Adding your user to the docker group lets you run docker and docker compose commands directly. The Supabase setup script expects this.
- Confirm Docker works without sudo by running:
docker run hello-world
- You should see a message that says Hello from Docker! followed by an explanation of what happened. This confirms Docker is fully operational.
Permission denied when running without sudo?
If you see a permission error after running newgrp docker, try logging out of your SSH session and reconnecting. The group membership sometimes requires a fresh login to take effect.
help me fix Docker permission denied errors on Ubuntu
Your server is ready with Docker Engine and Docker Compose. Next up, you will deploy the full Supabase stack using Docker Compose.
Deploy Self-Hosted Supabase
Your server now has Docker ready to run containers. The next goal is to deploy the full Supabase stack on your server so your apps have a database, authentication, and API layer that you fully control.
Supabase provides an official quick-start script that handles cloning the configuration files, generating secure credentials, and pulling all the Docker images. You will run this script, start the services with Postgres 17, and confirm everything is healthy.
In this step, get ready to:
- Run the Supabase quick-start script to clone and configure the project.
- Start the Supabase stack with Postgres 17 using Docker Compose.
- Locate and note your generated API credentials.
Run the Supabase quick-start script
The official quick-start script automates the entire setup process. It sparse-clones the Supabase Docker configuration, creates a project directory, generates all secrets (including JWT key pairs), and pulls the container images.
- In your SSH session, run the official Supabase quick-start script by entering this command:
curl -fsSL https://supabase.link/setup.sh | sh
- The script will prompt you for four URL values. For each prompt, enter your server's public IP address with the http:// prefix.
- When prompted for SUPABASE_PUBLIC_URL, enter http:// followed by your server's public IP address (e.g., http://192.168.1.100).
- Enter the same URL for API_EXTERNAL_URL and SITE_URL when prompted.
- For PROXY_DOMAIN, enter just the IP address without the http:// prefix.
What does this script do?
The script performs five tasks automatically. It installs any missing prerequisites (yours are already installed, so it skips this). It sparse-clones the docker/ directory from the Supabase repository.
It creates a project directory called supabase-project in your current working directory. It generates all secrets including DASHBOARD_PASSWORD and an asymmetric JWT signing key pair. Finally, it pulls all the Docker images.
- After the script finishes, move into the project directory it created by running:
cd supabase-project
- Confirm the configuration files are present by running:
ls
You should see docker-compose.yml, docker-compose.pg17.yml, and a .env file listed in the output.
Script failed or directory not found?
Make sure you have a stable internet connection. The script downloads several hundred MB of Docker images.
If you see a permissions error, confirm Docker is accessible without sudo by running docker ps. If that fails, your user may not be in the docker group from Step 1.
Help me troubleshoot the Supabase quick-start script failing on my Ubuntu server.
Start the stack with Postgres 17
The default Postgres version is switching to 17 this week. Starting fresh with Postgres 17 now avoids a forced upgrade later.
- Start the full Supabase stack with the Postgres 17 override by running this command:
docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d
What does this command do?
The -f flags layer two compose files together. The base docker-compose.yml defines all the Supabase services (PostgREST, Auth, Realtime, Storage, Studio). The docker-compose.pg17.yml override swaps the default Postgres image for supabase/postgres:17.6.1.084.
The -d flag runs all containers in the background so they persist after you close the terminal.
- Wait about 30 seconds for services to initialize, then verify all containers are healthy by running:
docker compose ps
You should see all services listed with a status of running or healthy. Look for services including db, rest, auth, realtime, storage, and studio.
- Open a browser on your Windows PC and navigate to your server's IP address on port 8000:
http://[[SERVER_IP="your server's public IP address"]]:8000
- When prompted for credentials, enter supabase as the username and the DASHBOARD_PASSWORD value from your .env file as the password (you will retrieve this in the next substep).
What is Studio?
Supabase Studio is a web-based dashboard for managing your database. It lets you browse tables, run SQL queries, and manage authentication. Access is protected by HTTP basic authentication using the DASHBOARD_USERNAME and DASHBOARD_PASSWORD from your .env file.
Can't reach Studio in the browser?
Run docker compose ps again and check that all services show a healthy status. If any are restarting, run docker compose logs studio to see what went wrong.
Confirm port 8000 is not blocked by your server's firewall. You can temporarily allow it with sudo ufw allow 8000/tcp. You will remove this rule in the next step when you set up HTTPS through Caddy.
Help me troubleshoot why I can't access Supabase Studio on port 8000.
Locate your generated credentials
The quick-start script generated all the API keys and passwords your apps need to connect. These credentials live in the .env file inside your project directory.
- View the key credentials by running this command:
grep -E 'SUPABASE_PUBLISHABLE_KEY|SUPABASE_SECRET_KEY|POSTGRES_PASSWORD|DASHBOARD_PASSWORD' .env
What are these keys?
SUPABASE_PUBLISHABLE_KEY is the client-side API key (replaces the legacy ANON_KEY). Your Next.js apps use this for browser-side requests.
SUPABASE_SECRET_KEY is the server-side API key (replaces the legacy SERVICE_ROLE_KEY). Your server-side code uses this for privileged operations.
POSTGRES_PASSWORD is the database superuser password. DASHBOARD_PASSWORD is what you use to log into Studio in the browser.
- Copy these four values somewhere safe (a password manager or a temporary notes file on your local machine). You will need them in Step 5 when updating your Vercel environment variables.
Legacy key names still work
If your existing apps reference ANON_KEY or SERVICE_ROLE_KEY, the .env file also contains these legacy HS256 JWT keys. Both the new opaque keys and the legacy keys are accepted by the API gateway.
Grep returns empty results?
Make sure you are inside the supabase-project directory. Run pwd to check your current location. If you see a different directory, run cd ~/supabase-project to navigate there.
If the .env file is missing entirely, the quick-start script may not have completed successfully. Re-run it from your home directory.
Help me find my Supabase credentials in the .env file.
Your self-hosted Supabase stack is now live. Next up, you will configure HTTPS with a custom domain so your apps can connect securely over the internet.
Configure HTTPS and DNS
Your self-hosted Supabase is running, but it is only accessible over HTTP on your local network. Your Next.js apps on Vercel need a secure, internet-accessible endpoint to connect to.
In this step, you will point a subdomain at your server and set up Caddy as a reverse proxy. Caddy automatically provisions a Let's Encrypt TLS certificate, giving you production-grade HTTPS with zero manual certificate management.
In this step, get ready to:
- Add a DNS A record pointing your subdomain to the server.
- Update your environment variables for HTTPS and open firewall ports.
- Start the Caddy reverse proxy for automatic TLS.
Add a DNS A record for your subdomain
Before Caddy can issue a TLS certificate, it needs a real domain name that resolves to your server. You will create a DNS A record that points a subdomain to your server's public IP address.
- Find your server's public IP address by running this command in your SSH session:
curl -4 ifconfig.me
What does this command do?
This calls an external service that returns your server's public-facing IPv4 address. You will need this IP for the DNS record you create next.
- Copy the IP address from the output.
- Open your domain registrar or DNS provider (for example, Vercel DNS, Cloudflare, or Namecheap).
- Navigate to the DNS settings for your domain.
- Create a new A record with the following values:
- Name/Host: db (this creates the subdomain db.yourdomain.com).
- Type: A.
- Value: paste your server's public IP address.
- TTL: leave at default (or set to Auto).
- Save the DNS record.
Why a subdomain instead of the root domain?
Using a subdomain like db.yourdomain.com keeps your database API separate from your main website. This is a common pattern for backend services in production.
DNS propagation can take a few minutes
Most DNS providers propagate A records within 1-5 minutes. You can verify propagation by running dig db.yourdomain.com from your server and checking that the IP matches.
Update environment variables and open firewall ports
Supabase needs to know its own public URL so that services like Auth and Realtime generate correct callback URLs. You also need to open ports 80 and 443 on the server firewall so Caddy can serve traffic and complete the Let's Encrypt challenge.
- Open the .env file in your Supabase project directory by running:
nano ~/supabase/.env
- Find the lines for SUPABASE_PUBLIC_URL, API_EXTERNAL_URL, and SITE_URL.
- Update all three to use your HTTPS subdomain:
SUPABASE_PUBLIC_URL=https://[[SUPABASE_DOMAIN="db.yourdomain.com"]]
API_EXTERNAL_URL=https://[[SUPABASE_DOMAIN="db.yourdomain.com"]]
SITE_URL=https://[[SUPABASE_DOMAIN="db.yourdomain.com"]]
- Also find and update the PROXY_DOMAIN and CERTBOT_EMAIL lines. These tell Caddy which domain to provision the certificate for:
PROXY_DOMAIN=[[SUPABASE_DOMAIN="db.yourdomain.com"]]
CERTBOT_EMAIL=[[YOUR_EMAIL="admin@yourdomain.com"]]
What are PROXY_DOMAIN and CERTBOT_EMAIL?
PROXY_DOMAIN tells Caddy which domain name to serve and request a certificate for.
CERTBOT_EMAIL is the email address Let's Encrypt uses to send certificate expiry warnings. Use any valid email you check.
- Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
- Open ports 80 and 443 on the server firewall by running:
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
Why both port 80 and port 443?
Port 443 serves your HTTPS traffic. Port 80 is needed for the Let's Encrypt ACME challenge, which proves you control the domain. Caddy also uses port 80 to redirect HTTP visitors to HTTPS automatically.
- Verify the firewall rules were added by running:
sudo ufw status
You should see 80/tcp and 443/tcp listed as ALLOW in the output.
Firewall rules not showing up?
If ufw status shows Status: inactive, the firewall is not enabled yet. Run sudo ufw allow 22/tcp first (to keep your SSH connection alive), then sudo ufw enable to activate it.
ask
Start the Caddy reverse proxy
Supabase ships a pre-configured docker-compose.caddy.yml override that adds a Caddy container to the stack. This container sits in front of the API gateway, terminates TLS, and proxies requests to the internal services.
- Stop the currently running stack so the new environment variables take effect:
cd ~/supabase && docker compose -f docker-compose.yml -f docker-compose.pg17.yml down
- Restart the stack with the Caddy override added by running:
docker compose -f docker-compose.yml -f docker-compose.pg17.yml -f docker-compose.caddy.yml up -d
What does this command do?
Docker Compose merges all three configuration files into a single stack. The base file defines all Supabase services, the PG17 override swaps in the Postgres 17 image, and the Caddy override adds the reverse proxy container with your domain and certificate configuration.
- Verify all containers are healthy by running:
docker compose ps
You should see all services listed as running or healthy, including the new Caddy container.
- Open a browser on your Windows PC and navigate to your HTTPS subdomain (for example, https://db.yourdomain.com).
You should see the Supabase Studio login page served over a secure HTTPS connection. The browser's address bar should show a lock icon confirming a valid TLS certificate.
Seeing a certificate error or connection refused?
If your browser shows a certificate warning, DNS may not have propagated yet. Wait a few minutes and try again.
If the connection is refused entirely, check that ports 80 and 443 are open on your server's firewall with sudo ufw status. Also verify your DNS A record points to the correct IP by running dig db.yourdomain.com from the server.
Check the Caddy logs for errors by running docker compose logs caddy.
Help me troubleshoot my Caddy reverse proxy not serving HTTPS for my self-hosted Supabase
Your self-hosted Supabase is now accessible over HTTPS with a valid TLS certificate. Next up, you will migrate your existing database from managed Supabase to this new self-hosted instance.
Migrate Your Database
Your self-hosted Supabase is live and accessible over HTTPS. The next step is moving your existing data from the managed Supabase project into your own instance.
PostgreSQL's pg_dump tool exports your entire database (schema, tables, and data) into a single SQL file. You'll dump everything from your managed project and import it directly into the self-hosted Postgres container.
In this step, get ready to:
- Export your schema and data from managed Supabase using pg_dump.
- Import the database dump into your self-hosted Postgres instance.
- Test the PostgREST API to confirm your data is accessible.
Export your database from managed Supabase
Your managed Supabase project holds all your tables, rows, and schema definitions. You need to extract all of that into a portable SQL file that your self-hosted instance can consume.
- In your browser, open your managed Supabase dashboard.
- Navigate to Settings > Database > Connection string.
- Copy the connection string. It looks like postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres.
What is this connection string?
The connection string is the address and credentials your server uses to reach the managed Supabase database over the internet. It contains the username, password, host, port, and database name all in one URI.
- Back in your SSH session, install the PostgreSQL client tools by running this command:
sudo apt install -y postgresql-client
- Export your full database by running this command, replacing your-database-password with your managed Supabase database password and your-project-ref with your project reference from the connection string:
pg_dump "postgresql://postgres:your-database-password@db.your-project-ref.supabase.co:5432/postgres" --clean --if-exists > ~/backup.sql
What does this command do?
- pg_dump connects to your remote managed Supabase database and exports everything.
- --clean adds DROP statements before each CREATE, so re-importing won't conflict with existing objects.
- --if-exists prevents errors if an object doesn't already exist in the target database.
- > ~/backup.sql redirects the output into a file in your home directory.
- Confirm the backup file was created by running:
ls -lh ~/backup.sql
You should see backup.sql listed with a file size greater than 0. The size depends on how much data your managed project contains.
Command not found or connection refused?
- If you see a "command not found" error for pg_dump, make sure the sudo apt install -y postgresql-client command completed successfully.
- If you see a connection refused error, double-check your connection string for typos. The password and project reference must match exactly what the Supabase dashboard shows.
- If the file size is 0 bytes, the export failed silently. Re-run the command and check for error messages in the terminal output.
Help me troubleshoot my pg_dump export from managed Supabase. I ran the command but got an error or an empty file.
Import the dump into your self-hosted instance
Now that you have the SQL file, you'll feed it into the Postgres container running inside your self-hosted Supabase stack. This recreates all your tables and populates them with your data.
- Make sure you're in your Supabase project directory by running:
cd ~/supabase
- Import the backup into your self-hosted database by running:
docker compose exec -T db psql -U postgres -d postgres < ~/backup.sql
What does this command do?
- docker compose exec -T db runs a command inside the running Postgres container (db is the service name). The -T flag disables interactive terminal mode so piped input works.
- psql -U postgres -d postgres opens the PostgreSQL shell as the postgres superuser connected to the postgres database.
- < ~/backup.sql pipes the SQL file as input, executing every statement in order.
You may see some notices or warnings during the import. This is normal. The --clean flag from your export generates DROP statements that may reference objects not yet present in a fresh database.
- Open your Supabase Studio dashboard in your browser at https://db.yourdomain.com.
- Click Table Editor in the left sidebar.
- Verify that your tables and data appear. You should see the same tables and row counts as your managed Supabase project.
Tables not showing up in Studio?
- Make sure you selected the correct schema in Studio. Click the schema dropdown (usually set to public) and confirm your tables are under that schema.
- If you see permission errors during import, verify you ran the command with -U postgres (the superuser).
- If the import appeared to succeed but tables are empty, re-check that your backup.sql file has actual content by running head -50 ~/backup.sql to inspect the first 50 lines.
Help me figure out why my imported tables aren't showing in Supabase Studio after running the psql import.
Test the PostgREST API
Your tables exist in the database, but your apps don't talk to Postgres directly. They use the PostgREST API layer that translates HTTP requests into SQL queries. You need to confirm this layer is serving your migrated data correctly.
- Test the API by running this command, replacing your-anon-key-here with your SUPABASE_PUBLISHABLE_KEY (or ANON_KEY) from the .env file, and your-table-name with one of your actual table names:
curl -H "apikey: your-anon-key-here" https://[[DOMAIN="db.yourdomain.com"]]/rest/v1/[[TABLE_NAME="your-table-name"]]?limit=1
What does this request do?
- curl sends an HTTP GET request to your self-hosted PostgREST endpoint.
- The apikey header authenticates the request using your publishable key (the same key your apps use client-side).
- /rest/v1/ is the PostgREST base path. The table name after it tells PostgREST which table to query.
- ?limit=1 restricts the response to a single row so you can quickly confirm data is flowing.
You should see a JSON array containing one row of data from your table. This confirms PostgREST is reading from your migrated database correctly.
Getting an empty array or an error?
- An empty array [] means PostgREST connected successfully but found no rows. Double-check your table name for typos, or try a different table you know has data.
- A 401 or "Invalid API key" error means the apikey value doesn't match what's in your .env file. Copy the SUPABASE_PUBLISHABLE_KEY value exactly.
- A connection refused error means the Caddy proxy or PostgREST container might be down. Run docker compose ps to check that all services show as healthy.
Help me debug my self-hosted Supabase PostgREST API returning an error or empty response.
Your database is fully migrated and the API is serving data. Next up, you'll point your existing Next.js apps at this self-hosted instance so they start using your own infrastructure.
Connect Your Next.js Apps
Your self-hosted Supabase is running, secured with HTTPS, and filled with your migrated data. The last piece of the puzzle is pointing your existing apps at this new infrastructure.
Both your Next.js apps on Vercel still reference your old managed Supabase URL and keys. You need to swap three environment variables on each project and trigger a redeployment. Because your apps use the @supabase/supabase-js client, zero code changes are needed.
In this step, get ready to:
- Update the staff dashboard's environment variables on Vercel.
- Update the user enrollment site's environment variables and redeploy both apps.
- Verify both apps work end-to-end against your self-hosted Supabase.
Gather your self-hosted credentials
Before heading to Vercel, you need your self-hosted keys handy. These live in the .env file inside your Supabase project directory on the server.
- In your SSH session, display the relevant keys by running this command:
grep -E 'SUPABASE_PUBLISHABLE_KEY|SUPABASE_SECRET_KEY|SUPABASE_PUBLIC_URL' ~/supabase/.env
- Copy the values of SUPABASE_PUBLIC_URL, SUPABASE_PUBLISHABLE_KEY, and SUPABASE_SECRET_KEY somewhere accessible (a text file or clipboard manager).
Which key is which?
SUPABASE_PUBLISHABLE_KEY is the client-side key (replaces your old ANON_KEY). It goes into NEXT_PUBLIC_SUPABASE_ANON_KEY on Vercel.
SUPABASE_SECRET_KEY is the server-side key (replaces your old SERVICE_ROLE_KEY). It goes into SUPABASE_SERVICE_ROLE_KEY on Vercel.
Update the staff dashboard environment variables on Vercel
Vercel environment variables tell your Next.js app where to find its database at build time and runtime. Changing these variables and redeploying is all it takes to switch your backend.
- Open the Vercel dashboard in your browser and select your staff dashboard project.
- Click Settings in the left sidebar.
- Select Environment Variables in the sidebar.
- Find NEXT_PUBLIC_SUPABASE_URL in the list. Click the three dots to the right and select Edit.
- Replace the old value with your self-hosted URL: https://db.yourdomain.com (your actual subdomain).
- Ensure Production is selected as the environment, then click Save.
- Repeat for NEXT_PUBLIC_SUPABASE_ANON_KEY. Replace the old value with your SUPABASE_PUBLISHABLE_KEY from the server.
- Repeat for SUPABASE_SERVICE_ROLE_KEY. Replace the old value with your SUPABASE_SECRET_KEY from the server.
Check for extra spaces
When pasting keys, make sure no trailing spaces or newlines sneak in. Extra whitespace in environment variable values causes silent authentication failures.
Update the enrollment site and redeploy both apps
Your user enrollment site uses the same three environment variables. After updating both projects, you need to trigger a redeployment so the new values take effect.
- Return to the Vercel dashboard home and select your user enrollment site project.
- Click Settings in the left sidebar, then select Environment Variables.
- Update NEXT_PUBLIC_SUPABASE_URL to your self-hosted URL (same value as the staff dashboard).
- Update NEXT_PUBLIC_SUPABASE_ANON_KEY to your SUPABASE_PUBLISHABLE_KEY.
- Update SUPABASE_SERVICE_ROLE_KEY to your SUPABASE_SECRET_KEY.
- Click Save after each change.
Now trigger a redeployment for both projects.
- On the enrollment site project, click Deployments in the left sidebar.
- Find your latest production deployment, click the three dots (ellipsis icon), and select Redeploy.
- In the Redeploy to Production dialog, click Redeploy.
- Navigate back to your staff dashboard project and repeat the same redeployment steps.
Why do I need to redeploy?
Vercel bakes environment variables into each deployment at build time. Changing a variable in settings does not affect existing deployments. A fresh redeployment picks up the new values.
Don't see the Redeploy option?
Vercel may show a toast notification prompting you to redeploy after saving variable changes. If you see it, click Redeploy in the toast instead.
If neither appears, confirm you saved the environment variable (check that the updated value shows in the list), then try the Deployments sidebar route again.
Help me redeploy my Vercel project after updating environment variables.
Verify both apps work end-to-end
With both redeployments complete, your apps are now reading from your self-hosted Supabase. Time to confirm everything works.
- Open your staff dashboard in the browser and log in.
- Confirm that your data loads correctly (tables, lists, or records you expect to see).
- Open the user enrollment site and test a user action (for example, submitting an enrollment form or signing up).
- Confirm the action completes successfully.
Finally, check that your self-hosted instance is actually receiving the requests.
- Switch back to your SSH session on the server.
- Check the Supabase logs by running this command:
cd ~/supabase && docker compose logs --tail=20 rest
What am I looking for?
You should see recent HTTP requests in the PostgREST container logs. Lines showing GET or POST requests to your tables confirm that your apps are hitting the self-hosted instance.
If the logs are empty, you can also open Supabase Studio at https://db.yourdomain.com and check the table editor. Any data written by your enrollment test should appear in the relevant table.
App showing a blank page or authentication errors?
Double-check that NEXT_PUBLIC_SUPABASE_URL uses https:// (not http://). Missing the protocol prefix causes the Supabase client to fail silently.
Verify that the NEXT_PUBLIC_SUPABASE_ANON_KEY value matches SUPABASE_PUBLISHABLE_KEY from your server's .env file character for character. A mismatch causes 401 Unauthorized responses.
If your enrollment site works but the staff dashboard does not (or vice versa), confirm you updated all three variables on both projects.
Help me debug why my Next.js app is not connecting to my self-hosted Supabase after updating Vercel environment variables.
Secret mission
Automated Daily Backups
Your self-hosted Supabase is live, but what happens if the database gets corrupted or accidentally wiped? In this secret mission, you'll write a bash script that automatically backs up your Postgres database every night and cleans up old backups to save disk space.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your own hardware. There are no ongoing cloud costs for the Supabase services, Caddy proxy, or Let's Encrypt certificates.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers running self-hosted Supabase (Postgres 17, PostgREST, Auth, Realtime, Storage, Studio, Caddy).
- DNS A record for your subdomain pointing to your server's public IP.
- Supabase project directory (~/supabase/).
- Backup directory and cron job (/opt/supabase-backups/).
- Migration dump file (~/backup.sql).
- Updated Vercel environment variables on both Next.js apps.
Keep everything running
No action needed. Choose this if your self-hosted Supabase is actively serving your apps and you want it to continue.
- The Docker containers are configured to auto-restart on boot. Your Supabase instance stays available without manual intervention.
- Monitor container health anytime by running docker compose ps from your ~/supabase/ directory.
- Check logs with docker compose logs if you notice issues.
- Your automated backup cron job continues running daily at 2:00 AM, keeping the last 7 days of backups.
- Let's Encrypt certificates auto-renew through Caddy. No manual certificate management needed.
Pause - I'll come back to this later
Stop all services to free up system resources while keeping your data intact. Restart whenever you are ready.
- SSH into your server and navigate to your Supabase project directory by running:
cd ~/supabase
- Stop all containers by running:
docker compose -f docker-compose.yml -f docker-compose.pg17.yml -f docker-compose.caddy.yml down
- Your data persists in the ./volumes/db/data directory. Nothing is lost.
- When you are ready to resume, run the same start command from the ~/supabase/ directory:
docker compose -f docker-compose.yml -f docker-compose.pg17.yml -f docker-compose.caddy.yml up -d
- Your apps on Vercel will return errors while the containers are stopped. They will resume working once you bring the stack back up.
Delete - I don't want to use this again
Remove all project resources from your server and revert your apps to their previous state.
- SSH into your server and navigate to your Supabase project directory by running:
cd ~/supabase
- Stop all containers and remove their volumes by running:
docker compose -f docker-compose.yml -f docker-compose.pg17.yml -f docker-compose.caddy.yml down -v
- Remove the cron job by running:
crontab -l | grep -v '/opt/supabase-backups/backup.sh' | crontab -
- Remove the backup directory and migration dump by running:
sudo rm -rf /opt/supabase-backups/
rm ~/backup.sql
- Remove the Supabase project directory by running:
rm -rf ~/supabase/
- Close the firewall ports you opened by running:
sudo ufw delete allow 80/tcp
sudo ufw delete allow 443/tcp
- Remove the DNS A record for db.yourdomain.com in your domain registrar or Vercel DNS settings.
- In your Vercel dashboard, revert the environment variables on both apps (staff dashboard and user enrollment site) back to your managed Supabase credentials if you still have that project.
Nice Work!
Nice Work!
You just took full ownership of your database infrastructure. Your apps are running against a self-hosted Supabase instance on hardware you control, with HTTPS, migrated data, and automated backups.
You've learned how to:
- Deploy a full self-hosted Supabase stack (Postgres 17, PostgREST, Auth, Realtime, Storage, Studio) with Docker Compose and configure HTTPS using Caddy on your Ubuntu server.
- Migrate an existing PostgreSQL database (schema and data) from a managed Supabase project to your self-hosted instance using pg_dump.
- Connect production Next.js apps on Vercel to your self-hosted Supabase with zero code changes by updating environment variables.
- Secret Mission: Configured automated daily database backups with a bash script, cron job, and 7-day retention policy.
Ready to quiz yourself?