VPS Tutorials
Self-Hosting Supabase on a VPS (2026): Docker Compose, HTTPS, Backups and Configuration Cost Guide
How big a VPS do you need for self-hosted Supabase? This article compares the cost and operational boundaries of the cloud version versus self-hosting, configures keys, Caddy HTTPS, SMTP, Storage, backups and upgrades following the official Docker process, and troubleshoots OOM, connection pooling and email failures.

If you're just building a small MVP and nobody on the team wants to watch backups and upgrades long-term, don't rush into self-hosting — just use Supabase Cloud and save yourself the hassle. Self-hosting on a VPS is only worth starting when you clearly need data control, stable load, special network policies, or you must keep components and backup cadence in your own hands.
This is also where many people miscalculate: self-hosted Supabase is not "buy a 2-core 4GB machine and you save money" — it means taking on responsibility for the database, gateway, auth, object storage, email, backups and upgrades all at once. The following is based on the official 2026 Docker route, and tries to lay out the pitfalls clearly in one go.
| Option | What monthly cost mainly consists of | Operational responsibility | Backup and recovery | Upgrade cadence | Data location and network | Better suited for |
|---|---|---|---|---|---|---|
| Supabase Cloud | Platform plan, overage usage, add-on capabilities | Platform handles underlying service availability; you mainly manage the business | More platform capabilities; you focus on business-layer exports and drills | Follow the platform's cadence | Limited region choices, but no need to deal with public entry points and reverse proxy details | Small teams, MVPs, projects with no one on call |
| VPS self-hosting | VPS, off-site backups, SMTP, optional object storage, maintenance time | You handle Docker, HTTPS, monitoring, keys and recovery yourself | Database, Storage, Functions and keys are all on you | You decide when to upgrade, and you bear the rollback | You can choose the data center, DNS, entry point and firewall yourself, but you also have to cover the fallback | Teams with ops capability, relatively stable load, and a must to control data location |
Breaking it down a bit more:
- Monthly cost: the cloud version is mainly plan and overage usage; self-hosting must at least account for the VPS, off-site backups, SMTP, optional object storage, plus maintenance time.
- Backups: with the cloud version the platform does a lot for you; with self-hosting you must prove that the database, files and keys can all be restored.
- Upgrades: what takes a few clicks on the cloud version requires you to read
CHANGELOG.md, verify version combinations and prepare for rollback when self-hosting. - Data location: the cloud version follows the available regions; self-hosting lets you choose the data center and network policy yourself, but failures are also yours to handle.
So I'd suggest judging it this way: if you're still agonizing over "should I learn Docker, should I install Caddy too, is SMTP a hassle", you're probably better off starting with the cloud version. Self-hosting is only worth it when two premises hold at the same time: first, you really need data control, network boundaries or more flexible component integration; second, you can accept that before every future upgrade you'll back up, verify versions and prepare a rollback.
If you really are going to buy a machine, run through the basic configuration thinking first — you can also check out this VPS configuration selection guide.
According to the current official self-hosting stack, 2 vCPU, 4GB RAM, 40GB SSD is the starting point where the full set of components can run, and the current database baseline is Postgres 17. If you plan to run a production project long-term, I'd recommend starting from 4 vCPU, 8GB RAM, 80GB SSD.
Both tiers are only starting points, not capacity promises, and certainly not QPS guarantees. The reason is simple: Supabase is not a single-container application; it brings up at least Postgres, Studio, Kong, Auth, PostgREST, Realtime, Storage, imgproxy, Edge Runtime and Supavisor.
In other words, 2 cores 4GB is more like the line where "the full stack can land, be verified and run with small traffic"; 4 cores 8GB is the line where you won't be scrambling when doing upgrades, running backups and enabling monitoring. If you also run other Docker services on the same machine, be sure to leave memory headroom.
Don't just look at the system image size for disk either. The database data directory, WAL, Storage files, Functions and backup archives all keep growing, and 40GB can be eaten up quickly. Start at 80GB for production environments — it'll be much more comfortable later when doing backups and recovery drills.
Many people seeing Supabase for the first time mistakenly think it's just "Postgres + an admin panel". It isn't.
- Postgres 17 is the core data layer;
- Kong handles the API gateway;
- Auth handles sign-up, login and OAuth callbacks;
- PostgREST exposes the REST API;
- Realtime handles subscriptions and WebSocket;
- Storage handles files;
- imgproxy handles image transformations;
- Studio is the admin interface;
- Edge Runtime runs Functions;
- Supavisor handles connection pooling.
So this kind of project can't just focus on "whether the containers come up" — you also need to look at health checks, disk space, connection pool modes, SMTP and the backup chain. These peripheral overheads are the most easily overlooked in Docker production environments; for related thinking you can compare this Docker Compose production health check and resource limits guide.
Before getting started for real, prepare the following:
- A clean Linux VPS;
- Git, Docker Engine, Docker Compose v2;
- A proper domain already resolving to the VPS;
- Public entry only opens 80/443; SSH 22 only allows access from trusted IPs, bastion hosts, VPN or cloud firewall allowlists;
- A separate deployment directory and disk space that can grow sustainably.
"Only open 80/443" here is not a slogan — it's to avoid later exposing Kong's dev port, the database port and the connection pool port all at once. Many people self-hosting for the first time, as soon as the containers come up, casually put 5432, 6543, 8000 and 8443 on the public internet; it saves trouble short-term but is a source of incidents long-term.
For the main process, it's recommended to follow the official Docker directory directly, and use a sparse clone to grab only the docker/ part — don't pull the whole repository, and don't treat one-click scripts of unknown origin as the main path.
git clone --filter=blob:none --no-checkout --depth=1 --quiet https://github.com/supabase/supabase
cd supabase
git sparse-checkout init --cone
git sparse-checkout set docker
git checkout --quiet
cd ..
mkdir supabase-project
cp -rf supabase/docker/* supabase-project
cp supabase/docker/.env.example supabase-project/.env
cd supabase-project
Next, don't rush to up -d. The default values in .env.example are just development examples — especially email, URLs and keys. Copying them directly in production will cause problems with sign-up, password reset and OAuth all at once.
Also, the deployment directory is best kept separate, not mixed with miscellaneous applications. When doing backups, migrations, cleaning old packages and tightening permissions later, the clearer the directory boundaries, the less hassle.
The official helper scripts are already provided; run them directly in the copied Docker directory:
sh utils/generate-keys.sh
sh utils/add-new-auth-keys.sh --update-env
The second script will update .env after generating new auth keys, so it's easiest to run the scripts first and then go back to verify the variables. Then check .env at least group by group:
- Database password;
- JWT / API keys;
- Studio login credentials;
- Various encryption keys;
SUPABASE_PUBLIC_URL;API_EXTERNAL_URL;SITE_URL;- redirect allow-list;
- SMTP;
- optional S3-compatible storage credentials.
The best approach at this step is not "fill in whatever first, fix it when it errors", but to sort out the external addresses, email sending, keys and admin panel accounts all at once. Because many problems in Supabase are not single-point failures but chain failures: for example, if you only change SITE_URL and not API_EXTERNAL_URL, the console may open on the surface, but email links and OAuth callbacks are still wrong.
Two things are especially easy to get wrong here:
API_EXTERNAL_URLmust be a valid external address with a protocol, and the path must end with/auth/v1.SITE_URLpoints to your application origin, not just any console address.
Also, SERVICE_ROLE_KEY can only go on the server side — don't put it in frontend build variables, static pages or public repositories.
According to the current official baseline, Supavisor's 5432 is session mode, 6543 is transaction mode. Neither port should be exposed directly to the whole public internet, and certainly not written as 0.0.0.0/0 in the security group.
If you only access from inside containers, just use the default internal network; if you really need remote database access, at least add trusted source rules, VPN, SSH tunnel or private network.
To emphasize once more: don't treat "can connect to the database from the external internet" as the deployment success criterion. For most businesses, the only thing that should really be exposed to the public internet is the site entry point; the database and connection pool should stay within the internal network boundary as much as possible.
Keep the official order for startup and health check commands:
docker compose pull
docker compose up -d --wait
docker compose ps
If containers keep restarting at this step, don't think about resetting data first — check logs and disk first. reset.sh is not an everyday troubleshooting button; misuse means wiping data.
For production environments, it's recommended to add the official Caddy overlay directly:
sh run.sh config add caddy
sh run.sh start
At this stage you need to confirm three things:
- DNS already points to the current VPS;
- The public internet only needs
80/443; - Realtime's WebSocket also goes through the reverse proxy over HTTPS.
The 8000/8443 common in development, and the database-related 5432/6543, should not be directly open to the whole internet. Kong's dev port should be pulled back, and the external entry point handed uniformly to Caddy. To sort out the reverse proxy and automatic certificate process, you can compare this Caddy reverse proxy automatic HTTPS guide.
If you also need Realtime subscriptions, the admin panel and the API available at the same time, don't open a second set of public ports either — letting Caddy handle TLS and the entry point uniformly is clearer. That way, when certificate, redirect or WebSocket problems occur, you only need to watch one layer of entry.
Email first. Many people find after a successful install that sign-up emails aren't sent and password reset links are invalid — essentially it's not that Supabase is "broken", but that they're still using the fake SMTP configuration in .env.example. Production must switch to a real SMTP service and verify the sender domain, port, authentication method and reply address.
Then URLs. SUPABASE_PUBLIC_URL, API_EXTERNAL_URL, SITE_URL and the OAuth callback must be valid addresses within the same external logic, otherwise the most common problem is: the console opens, but email links jump to the wrong domain, or third-party login callbacks return 400.
One more piece of experience-based judgment: if you'll deploy the application and Supabase on separate domains in the future, you should sort out these addresses even earlier. Being lazy in development and hardcoding a temporary address often means reworking email templates, redirect allowlists and third-party login together when switching to the production domain at launch.
Storage by default puts files in the local ./volumes/storage/. If it's just a single-machine project, this is the least hassle; only switch to an S3-compatible backend, such as S3 or R2, when you clearly need to decouple files from the host lifecycle, want cross-machine sharing, or your backup strategy already requires object storage. There's no need to stuff another MinIO onto the same VPS for the sake of "architectural completeness". For when to use object storage, you can follow this when a VPS needs object storage.
The easiest thing to miss in self-hosted Supabase is not SQL, but the other files needed to restore a complete instance. At minimum, save the following categories separately:
- Database logical backup;
pgsodium_root.key;volumes/storage;volumes/functions;.env,docker-compose.yml,docker-compose*.yml,volumes/api,volumes/proxy,volumes/poolerand other currently effective configurations.
You can start with this set of commands:
mkdir -p backups
docker exec supabase-db pg_dumpall -h localhost -U supabase_admin > "backups/supabase-$(date +%F).sql"
docker compose run --rm db cat /etc/postgresql-custom/pgsodium_root.key > "backups/pgsodium_root.key"
tar -czf "backups/supabase-files-$(date +%F).tar.gz" volumes/storage volumes/functions
tar -czf "backups/supabase-config-$(date +%F).tar.gz" .env docker-compose.yml docker-compose*.yml volumes/api volumes/proxy volumes/pooler
These files must all be encrypted or strictly access-controlled, and copied to another fault domain — they can't just sit on the same VPS. .env and pgsodium_root.key especially must not be committed to Git. For how to do recovery drills in a backup strategy, you can refer to this VPS backup, recovery and drill guide.
Many teams say they "have backups", but in reality they just export a SQL file to the local disk every day. When a real problem occurs, you still have to face Storage files gone, Functions lost, the encryption root key missing, and SMTP configuration forgotten. Whether you can restore a complete instance doesn't depend on whether pg_dumpall ran, but on whether you kept all these puzzle pieces together.
For production upgrades, the correct order should be: first read CHANGELOG.md and versions.md in the official Docker directory to confirm what this change affects; then do a complete backup; then update the official Compose configuration and pull the same set of tested versions; finally restart and verify health status. Also a reminder: some Restore docs still mention PG15, but the current Docker deployment guide, Docker's CHANGELOG.md/versions.md and the PG17 upgrade guide already treat PG17 as the current baseline — judge versions by the latter.
Never rush and just change the image to latest. Supabase's Docker directory is tested as grouped versions; chasing a single latest image on your own can easily mess up Auth, gateway or database compatibility.
Basic checks after an upgrade should include at least these:
docker compose ps
docker stats --no-stream
docker compose logs --since=30m kong auth db supavisor
df -h .
df -i .
If it's OOM, first check whether 2 cores 4GB is already maxed out by Studio, backup tasks, the database and business traffic together; if it's Storage upload failure, first check disk permissions, remaining space and the local volumes/storage; if it's an OAuth callback anomaly, first go back and check the external URL and callback allowlist.
The core idea here is: check status first, then decide whether to roll back. Don't just delete containers or rerun initialization scripts as soon as you see an anomaly after an upgrade. As long as the database volume and configuration aren't protected first, troubleshooting easily turns into a second incident.
After self-hosted Supabase goes live, at minimum watch four types of signals:
- Whether containers stay healthy;
- Whether memory is approaching the limit;
- Whether disk space and inodes are tight;
- Whether
kong,auth,db,supavisorkeep reporting errors.
If you don't have a complete monitoring stack yet, you can start with the basic inspection using the commands above. Once the business is stable, add more systematic collection and alerting — otherwise when the first OOM or certificate anomaly occurs, you can only guess. For the corresponding monitoring expansion thinking, you can later integrate a more complete collection system, but the premise is to get the most critical container and disk signals working first.
It can run the full stack, but it's more suitable for verification, small traffic or internal projects. As long as you also need to back up, read logs and casually run other containers, 4 cores 8GB will be much more stable.
Don't reinstall first. First check docker stats --no-stream and the logs of the last 30 minutes to confirm whether memory is maxed out by Studio, backup tasks, the database and business peaks together. The most common solution for a 2-core 4GB machine is not "tweak a bunch of mysterious parameters", but to reduce concurrency, stop unrelated containers, and if necessary upgrade directly to 4 cores 8GB.
According to the current official baseline, Supavisor's 5432 is session, 6543 is transaction. Most applications should choose according to the officially recommended scenarios first, and don't expose both to the public internet.
Check SMTP first, don't blame Auth first. Fake sender addresses, blocked ports and misconfigured TLS will all make sign-up and password reset fail together.
First compare SUPABASE_PUBLIC_URL, API_EXTERNAL_URL, SITE_URL, the redirect allow-list and the callback addresses in the third-party platform. Most 400 or wrong-page problems have their root in external URLs not being unified.
Check three things first: whether the local ./volumes/storage/ permissions are correct, whether disk and inodes are sufficient, and whether the upload entry still goes through the correct HTTPS reverse proxy. If you've already switched to S3 or R2, also check credentials, bucket permissions and endpoint.
When your main contradiction is still "get the product out first" rather than "I must control data location, network and upgrade cadence myself", prioritize the cloud version.
In one sentence: no ops staff and the project is still in the trial-and-error phase — use Supabase Cloud; the business is already stable and you're willing to bear backup, upgrade, SMTP and security responsibilities long-term — then go with VPS self-hosting.
What's really valuable about self-hosting is not "it looks like you saved a platform layer", but that you know why you're taking over these responsibilities and are prepared with the corresponding SOPs. As long as this premise isn't thought through, the cloud version is usually more cost-effective.
Conversely, if you already have clear data location requirements and want to bring the entry point, SMTP, object storage and backup strategy back into your own standard processes, then this route is worth implementing. Don't chase a one-step "perfect architecture" — getting the first version running stably with the official Docker, official key scripts, official Caddy overlay and a complete backup chain is more important than anything.