Build Supabase Pro
KINDAback to the verdict
The official Supabase stack running on one server behind TLS, with generated secrets rather than the shipped placeholders, Studio locked away from the public internet, nightly database and storage backups going off the machine, and a restore you have performed on a second server with the date recorded.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- have readyroughly 20 to 40 USD a month at 8 GB with most providers
Why Below this the stack starts and then dies under any real use. 8 GB is the honest recommendation once you have data.
Get it Any virtual server provider. Choose a region near your users and a plan you can resize later. open ↗
Verify
free -m reports 4 GB or more and docker runs - have readya few USD if you delete it the same day
Why The drill is worthless on the machine that already has your state. You can destroy this one afterwards.
Get it The same provider, the smallest plan that meets the memory floor. Create it for the drill and delete it after.
Verify
You can SSH to it and run Docker - have readyroughly 10 to 15 USD a year
Why Certificates need a hostname, and the API needs a stable address.
Get it Any registrar. Add an A record for the API hostname pointing at the server.
Verify
dig your hostname returns the server address - accounta few USD a month at these volumes; the first gigabytes are often free
Why Backups on the server die with the server, which is the exact scenario you are protecting against.
Get it Object storage from any provider, or a second host you control. Create a bucket and credentials scoped to write only into it.
Verify
A test file uploads and can be listed from your laptop - decidefree
Why The JWT secret is part of a working restore. Losing it means existing tokens and accounts do not survive.
Get it A password manager entry, or an encrypted file kept somewhere other than the server. Record the anon key, service role key, JWT secret, database password and dashboard password.
Verify
You can retrieve all five from somewhere that is not the server
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- You are not designing a schema here. What you are designing is the operational state, and it is worth writing down before you start. - secrets · POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_PASSWORD, all generated by you and none of them the shipped example values - volumes · the PostgreSQL data directory and the storage volume, which are the two things whose loss is unrecoverable - backups · dated database dumps and dated storage archives, in a destination that is not this server - restore log · the date of every restore drill you have performed, kept in the README
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
POSTGRES_PASSWORDsecret | required | generate-a-long-random-value | Generate with openssl rand. Never the shipped example. |
JWT_SECRETsecret | required | generate-a-long-random-value | Generate it, then derive the anon and service role keys from it. It is part of a working restore, so back it up. |
ANON_KEY | required | eyJhbGciOi... | Signed with your JWT secret. Public by design, but it must be yours rather than the published example. |
SERVICE_ROLE_KEYsecret | required | eyJhbGciOi... | Signed with your JWT secret. Bypasses row level security entirely, so it never reaches a browser. |
DASHBOARD_PASSWORDsecret | required | generate-a-long-random-value | Studio's own login. Change it even though Studio is not publicly exposed. |
SITE_URL | required | https://app.example.com | Where auth redirects send users after sign-in. |
BACKUP_DEST | required | s3://your-bucket/supabase | The off-server destination for both nightly artefacts. |
The build, in order
Server, Docker and a closed front door
A server exists, runs containers, and exposes nothing to the internet that you have not decided to expose.
Create a non-root user with sudo, disable password authentication for SSH, and enable unattended security updates.
terminaladduser deploy && usermod -aG sudo deploy systemctl status ssh
Use Docker's own repository rather than a distribution package, which is often several versions behind.
terminaldocker --version docker compose version
Allow SSH, 80 and 443 only. Then scan from your laptop. Trusting the configuration file is how people expose databases.
terminalufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable nmap -Pn YOUR_SERVER_IP
done when · tick each as it passeswatch out- Docker publishes ports by writing iptables rules that bypass the usual firewall front end, so a service you thought was blocked can be wide open. Verify with a port scan from a different machine rather than by reading your firewall configuration.
The stack up, with secrets that are yours
Supabase runs, and not one credential is the value that shipped in the example file.
Take only the docker directory. Copy .env.example to .env, then treat every line of it as something you must replace.
Files
docker/.envterminalgit clone --depth 1 https://github.com/supabase/supabase cp supabase/docker/.env.example supabase/docker/.env
A long random PostgreSQL password, a long random JWT secret, then derive the anon and service role keys by signing the documented payloads with that secret. Store them in a password manager as you go.
terminalopenssl rand -base64 48
Start the stack and read the health of every container. A container in a restart loop is the normal first outcome and the logs say why.
terminaldocker compose up -d docker compose ps
done when · tick each as it passeswatch out- The single most common self-hosting mistake is leaving the example values in place. The shipped anon key, service role key, JWT secret and dashboard password are published in the repository, so a stack running with them is readable and writable by anyone who finds it. Generate all of them before the first time the port is reachable, not after.
TLS, hostnames and a private Studio
The API is reachable over HTTPS at your domain, and the dashboard is not reachable by the public at all.
An A record for the API hostname. Caddy proxies to Kong on 8000 and obtains certificates on first request.
Files
CaddyfileBind its published port to 127.0.0.1 in the compose override, then reach it with ssh -L when you need it.
Files
docker-compose.override.ymlterminalssh -L 3000:localhost:3000 deploy@YOUR_SERVER
Curl the API host from your laptop, and try the Studio port from your laptop too. The second one must fail.
terminalcurl -I https://api.example.com/rest/v1/
done when · tick each as it passeswatch out- Studio talks to the stack with the service role key, which bypasses every row level security policy you will ever write. Exposing it to the internet behind only a password makes that password equivalent to the whole database.
Backups of both things that matter
The database and the uploaded files both leave the server on a schedule.
pg_dump from a container or from the host, gzipped and dated, run by cron or a systemd timer.
terminaldocker compose exec -T db pg_dump -U postgres postgres | gzip > db-$(date +%F).sql.gz
Tar the storage volume to the same destination with the same date stamp so the two halves of a restore are obviously paired.
terminaltar czf storage-$(date +%F).tar.gz -C ./volumes storage
Copy to object storage or another host. Then check each morning that a file with today's date exists, and alert when it does not.
Files
scripts/backup.shscripts/check_backup.sh
done when · tick each as it passeswatch out- A database dump does not contain uploaded files, and a storage archive does not contain the database. Backing up only one gives you a restore that produces rows pointing at files that are gone, or files nobody can find. Back up both, on the same schedule, and restore both in the drill.
The restore drill, actually performed
You have brought the whole thing back on a different server, and you know how long it took.
Same Docker setup, fresh clone of the stack, and the same environment file, which means the secrets are part of your backup and not only in your head.
Load the dump into the fresh database and unpack the storage archive into the storage volume before starting the storage service.
terminalgunzip -c db-2026-09-07.sql.gz | docker compose exec -T db psql -U postgres postgres
Compare row counts, download a file that existed before the backup, and sign in with an existing account. Then record the date and how long it took.
Files
README.md
done when · tick each as it passeswatch out- Restoring onto the same server proves almost nothing, because the parts you forgot are still sitting there. The drill has value precisely because the second machine has none of your undocumented state.
Monitoring, upgrades and the incident you will haveproduct builder
You find out about problems before your users do, and an upgrade is a planned event.
An external uptime check for the API, a query check for the database, and a disk threshold at around seventy five percent so there is time to react.
Back up, pull the new compose file, read the release notes, apply on the second server, verify, then apply on the live one. Written down, because you will do this while distracted.
Files
docs/upgrade.mdSet the Docker logging driver's max size and file count, or the container logs will quietly consume the disk over months.
Files
docker-compose.override.yml
done when · tick each as it passeswatch out- The stack's containers are versioned together and upgrading one service in isolation is how you get a database that the rest of the stack cannot talk to. Upgrade by moving the whole compose file forward, after a backup, and read the release notes for migration steps.
That is the whole plan for Supabase Pro. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A global edge network and the latency that comes with one.
- Managed database operations, patching, tuning and failover.
- Autoscaling, read replicas and connection pooling at scale.
- Point in time recovery. Nightly dumps mean a bad day can cost you a day.
- Anyone to call when it breaks.
- managed Postgres, backups, auth delivery, storage, edge functions, observability, and support
- global edge network
- managed databases
- autoscaling
- DDoS protection, support, and compliance
- Point in time recovery with continuous archiving, once nightly dumps stop feeling sufficient
- A staging stack restored from last night's backup, which also exercises the restore weekly
- Read replicas, but only after measuring that one server is genuinely the limit
Need the files? The project pack on the verdict page hands your agent the whole brief · more hosting.