Why self-host Supabase on a VPS
Supabase is not a black box: it is a set of open source services (PostgreSQL, GoTrue for auth, PostgREST for the API, Realtime, Storage, Envoy as a gateway, formerly Kong) orchestrated together. The cloud version bills per project and per monthly active user, and pauses inactive free projects. By self-hosting on a VPS, you get a complete backend-as-a-service without those limits, with direct access to your PostgreSQL for migrations, extensions (pgvector, PostGIS) and tuning. Ideal for an agency hosting several client apps, a bootstrapped SaaS looking to control costs, or any project requiring data sovereignty. You handle updates and backups yourself, in exchange for total freedom.
Concrete benefits of a self-hosted Supabase
- Complete backend: Postgres, Auth, Storage, Realtime and REST/GraphQL API in one stack.
- Direct access to the underlying PostgreSQL: SQL migrations, extensions and RLS policies without limits.
- No pausing or billing per monthly active user.
- Studio included: a web administration interface to manage tables, auth and buckets.
- AI extensions via
pgvectorfor RAG and semantic search, freely enabled. - Multi-projects on the same VPS: practical for an agency or development studio.
Hardware and software requirements
Self-hosted Supabase starts around ten containers (Postgres, Envoy, GoTrue, PostgREST, Realtime, Storage, Studio, Meta, Imgproxy...), making it a more resource-intensive stack than a plain database. Count a minimum of 2 vCPU and 4 GB of RAM for a test environment, and prefer 4 vCPU with 8 GB of RAM and 80 GB of SSD for production. On the software side: Ubuntu 22.04/24.04 LTS, Docker and Docker Compose v2, a domain name pointing to the VPS (e.g. api.myapp.com) to expose the gateway over HTTPS, and a reverse proxy such as Caddy, Traefik or Nginx to handle SSL. Also plan to generate strong secrets (JWT_SECRET, anon and service_role keys).
Deploy self-hosted Supabase with Docker
Clone the official repository and prepare the environment
Fetch the
dockerfolder from the Supabase repository, copy.env.exampleto.env, then generate unique secrets:POSTGRES_PASSWORD,JWT_SECRET, and theANON_KEY/SERVICE_ROLE_KEYkeys derived from the JWT. Never deploy with the example values.Configure URLs and the Studio password
In
.env, setSITE_URL,API_EXTERNAL_URLandSUPABASE_PUBLIC_URLwith your domain, and secure Supabase Studio withDASHBOARD_USERNAMEandDASHBOARD_PASSWORD: Studio gives full admin access to your project.Launch the stack
Start with
docker compose up -d, then follow service startup viadocker compose ps. The first Postgres initialisation and internal migration application take one to two minutes; check for the absence of errors indocker compose logs.Place a reverse proxy with SSL
Put Caddy or Traefik in front of the API gateway (Envoy, port 8000) to expose your API over HTTPS. Caddy automatically obtains and renews Let's Encrypt certificates: a simple
myapp.com { reverse_proxy localhost:8000 }block is enough. Never expose Postgres publicly. Yet the official compose publishes the gateway (8000) and the Supavisor pooler (5432 and 6543) on all interfaces, and ports published by Docker bypass ufw: prefix them with127.0.0.1:indocker-compose.yml(for example127.0.0.1:${POSTGRES_PORT}:5432), or close them in your provider's firewall.Enable RLS policies
Connect to Studio, create your tables, then enable Row Level Security on each one and define your policies. Without RLS, your
anonkey exposes all your data: this is the setting to configure before exposing anything.Back up Postgres and Storage
Schedule a daily
pg_dumpof the database and archive the Storage volume (file buckets) to external storage. Test a full restoration on a staging VPS before relying on it in production.
Supabase vs Appwrite: which self-hosted BaaS to choose?
Scroll the table
| Criterion | Supabase | Appwrite |
|---|---|---|
| Database | Native PostgreSQL, full SQL | MariaDB internally, document/collection API |
| Data model | Relational, Postgres RLS | Documents and collections, rule-based permissions |
| Auto-generated API | REST (PostgREST) + GraphQL | REST, GraphQL and multiple SDKs |
| Authentication | GoTrue, OAuth, magic links | Integrated auth, OAuth, teams, JWT |
| Resource footprint | Heavier (~10 containers) | Moderate, more compact stack |
| Vector search / AI | Native via pgvector | Not native, must be externalised |
| Best for | Relational apps, RAG, advanced SQL | Mobile/web apps oriented around documents |
| Learning curve | Comfortable if you know SQL | Fast for front/mobile developers |
Update Supabase with care: since images are versioned in docker-compose.yml, always do a full pg_dump and a VPS snapshot before a docker compose pull && docker compose up -d. Some version bumps touch the internal service schema. For production, isolate Postgres on a dedicated high-IOPS volume and note that pgvector ships with the image: it can be enabled at any time with create extension vector;, without rebuilding the database.
Deploy from the official Supabase stack, not a homemade compose
It is tempting to write a minimal docker-compose with only the few services you need. In practice, this breaks: the supabase/postgres image runs its own migration script (migrate.sh) that provisions the base roles (supabase_admin, authenticator, supabase_auth_admin...) and the entire auth schema, and expects Supabase SQL initialisation files to be mounted as files with variable expansion at run time. A standalone inline compose cannot reproduce this (Docker Compose interpolates variables inside inline configs, leaving the auth schema half-migrated and GoTrue failing with 'must be owner of function auth.uid()'). The robust approach — the one ServOrbit uses — is to deploy the official Supabase Docker files, pinned to a specific version, and let migrate.sh do the bootstrapping exactly as upstream intends.
Back your instance up automatically
Supabase's Docker deployment backs up nothing on its own — it is the gap self-hosters report most. Two mechanisms complement each other. The first, pg_dump, produces a full logical copy: run it from the host on a cron, write it to a volume separate from the database's own, and keep several generations. The second, WAL archiving, journals every transaction and lets you rewind to a precise moment rather than to the last dump. The first is enough for most projects; the second becomes necessary as soon as losing a day of writes is unacceptable. Either way, a backup is only worth what a restore proves: spin up a blank instance and replay your dump into it before you actually need to.
Trimming the stack: the analytics service
Until self-hosted release 0.5.0 (June 2026), the official Docker composition shipped Logflare, Supabase's internal analytics service, under the name analytics. It is now optional: removed from the default docker-compose.yml, it is enabled with the docker-compose.logs.yml override. It is essential neither to the API, nor to authentication, nor to storage: it is an observability tool. On an older instance it may still be running, and on a VPS with tight resources it often reports an unhealthy state without the rest of the stack suffering. First measure what it consumes on your setup with docker stats before deciding: if your monitoring already goes through an external tool, you lose nothing; if you relied on its logs, turn it off only after wiring up a replacement.
Kong→Envoy migration: what changes, and who it breaks
On 11 August 2026, with its self-hosted release 0.8.0, Supabase made Envoy the default API gateway for self-hosted installations; Kong moves to an optional override. Envoy had already shipped for several releases via docker-compose.envoy.yml — what changes is the default.
In docker-compose.yml the service is now called api-gw and the container supabase-envoy, but the kong network alias is kept: services that call the gateway by that name keep working. The HTTP port stays 8000, set through API_GW_HTTP_PORT, so your Caddy or Nginx reverse proxy needs no change.
⚠️ Supabase calls this a breaking change for a SUBSET of self-hosters, and you need to know whether you are one. Three cases: you relied on Kong's built-in HTTPS port 8443 — it is no longer shipped by default, TLS termination now goes through docker-compose.caddy.yml or docker-compose.nginx.yml; you had a customised volumes/api/kong.yml — routes and plugins must be ported to the Envoy configuration, which lives in volumes/api/envoy/; your scripts reference the gateway by service name. In all three cases you can also stay on Kong: sh run.sh config add kong.
Move to Envoy without breaking your gateway
Find out what you are running
Run
docker compose ps. Asupabase-kongcontainer means you are still on Kong;supabase-envoymeans the switch is done. Also check whethervolumes/api/kong.ymldiffers from the repository's — that file decides whether the migration costs you any work.Back up before touching the compose file
Take a full
pg_dumpand, if your host allows it, a VPS snapshot. Switching gateway does not touch the PostgreSQL schema, but it changes the entry path of every request: a fast rollback beats a live diagnosis.Decide: port or stay
If your
kong.ymlis the repository's, there is nothing to port. If it holds routes or plugins of your own, two paths: translate them into Envoy configuration undervolumes/api/envoy/, or stay on Kong withsh run.sh config add kongwhile you do. Staying is a legitimate decision, not a failure.Pull the new compose and restart
Pull the latest
dockerfolder from the official Supabase repository, thendocker compose down && docker compose up -d. Check thatapi-gwreacheshealthyand that the API answers on port 8000.Restore TLS if you relied on Kong's 8443
The built-in HTTPS port is no longer shipped. Add TLS termination with
docker-compose.caddy.ymlordocker-compose.nginx.yml, or let your existing reverse proxy handle it in front of port 8000. This is the most common breakage, and it only shows on the first direct HTTPS call.Check storage and signed URLs
Test a download from a private bucket through a pre-signed URL. If you get a 403 after the switch, look first at
Hostheader rewriting and at your storage service's public URL setting: that is where the two gateways differ.
Kong and Envoy, point by point
Scroll the table
| Aspect | Kong (before August 2026) | Envoy (default since) |
|---|---|---|
| Service in `docker-compose.yml` | `kong` | `api-gw` — `kong` kept as a network alias |
| Container | `supabase-kong` | `supabase-envoy` |
| HTTP port | 8000 | 8000 — via `API_GW_HTTP_PORT` |
| Built-in HTTPS port | 8443 | **removed** — TLS via `docker-compose.caddy.yml` / `.nginx.yml` |
| Configuration | `volumes/api/kong.yml` | `volumes/api/envoy/` (versioned YAML) |
| Opting back out | — | `sh run.sh config add kong` |
CVE-2026-31813: OIDC flaw in GoTrue — update now
A vulnerability was published in March 2026 in GoTrue (Supabase Auth, Supabase's authentication service) under the reference CVE-2026-31813. It affects ID token sign-in with the Apple and Azure providers: GoTrue validated the token against whatever issuer the token named, including an issuer controlled by the attacker.
In practice, an attacker who signs, from their own OIDC issuer, an ID token carrying a victim's email address obtains a valid session (AAL1 level) on that victim's account. Only instances with the Apple or Azure provider enabled are affected. Instances that only use email/password authentication or magic links are not exposed.
The fix is included in Supabase Auth (GoTrue) 2.185.0. To check your version: docker compose exec auth gotrue version. If your gotrue version is older than 2.185.0, update immediately.
Update GoTrue to fix CVE-2026-31813
Identify the current version
From your Supabase stack directory:
docker compose exec auth gotrue version. If the output shows a version < 2.185.0, your instance is exposed to CVE-2026-31813. Also note the overall stack version:grep 'SUPABASE_VERSION' .env(if pinned) ordocker compose images | grep supabase.Back up before updating
Before any update, back up the database:
(umask 077; docker compose exec -T db pg_dumpall -h localhost -U supabase_admin > /root/supabase-backup-$(date +%F).sql), never in/tmp, which every account on the server can read. Updating GoTrue does not involve a PostgreSQL schema migration, but a prior backup remains the rule before any version change in production.Update Supabase Auth to 2.185.0 or newer
In your
docker-compose.yml, update thesupabase/gotrueimage tag tov2.185.0or newer, or if following the full stack, pull the latest compatible version:docker compose pull && docker compose up -d auth. Updating GoTrue alone does not require restarting other services. Verify withdocker compose exec auth gotrue versionthat the new version is active.Verify OIDC authentication
After the update, test a complete authentication flow with each configured OIDC provider: login, logout, re-login. Check in GoTrue logs (
docker compose logs auth) that no error messages related toissvalidation appear under normal usage conditions.
Temporarily disable Apple and Azure if you cannot update immediately
If an immediate update is not possible (maintenance window required), reduce the exposure by temporarily disabling the Apple and Azure providers. In self-hosted, they are set through the auth service's environment variables: set GOTRUE_EXTERNAL_APPLE_ENABLED and GOTRUE_EXTERNAL_AZURE_ENABLED to false, then run docker compose up -d auth. Email/password authentication keeps working. This workaround is temporary: it does not fix the flaw, it only reduces the attack surface while you prepare the update.
PostgreSQL 15 → 17 migration: pin before you pull
On 17 June 2026, with its self-hosted release 0.6.0, Supabase switched the default PostgreSQL image in its official docker-compose.yml from version 15 to version 17 (a change announced on 18 May 2026 in discussion [#46080](https://github.com/orgs/supabase/discussions/46080)). This change is destructive if you ignore it: the vendor warns never to start PostgreSQL 17 on a data directory created by PostgreSQL 15. Pulling the new compose carelessly on an instance still on PG 15 leaves you with a database that no longer starts.
Second trap: the timescaledb, plv8, plcoffee and plls extensions are not built for the PG 17 image. None is installed by default; if you added one and need to keep it, the vendor advises not to run the upgrade. Check before going further: docker compose exec db psql -U postgres -c "\dx" lists the active extensions.
Migrate from PostgreSQL 15 to 17 without losing your data
Stay on PG 15 before any `docker compose pull`
First check your version:
docker compose exec db psql -U postgres -c "SELECT version();". If it starts with 15, keep the PG 15 image until the migration is done: the repository provides thedocker-compose.pg15.ymloverride, which pinssupabase/postgres:15.8.1.085. Add it withsh run.sh config add pg15, or pass it by hand:docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d. It is also the rollback target of the migration script.Back up the data and the pgsodium key
In a root shell, start with
umask 077: the files you create will be readable by you only. Copy the data directory:cp -a ./volumes/db/data ./volumes/db/data-manual-backup. Then back up the pgsodium root key, which lives in thedb-confignamed Docker volume, not in that directory:docker compose run --rm db cat /etc/postgresql-custom/pgsodium_root.key > ./pgsodium_root.key.backup. Without it, Vault secrets become unrecoverable. On top of that, a logical export:docker compose exec -T db pg_dumpall -h localhost -U supabase_admin > ./pg15_dump.sql. Copy these files to external storage.Check extensions and disk space
docker compose exec db psql -U postgres -c "\dx"lists the active extensions. Iftimescaledb,plv8,plcoffeeorpllsappear and you need them, stop there: they do not exist for PG 17. Otherwise, the script will offer to drop them. Also check free space: you need at least twice the database size, plus 5 GB.Fetch the up-to-date docker folder
Pull the latest version of the
dockerfolder from the official Supabase repository, which containsutils/upgrade-pg17.sh. Leave all the stack's containers running: the script requires it. Do not delete any Docker volume by hand, especiallydb-config, which holds the pgsodium key.Run the official migration
sudo bash utils/upgrade-pg17.sh. The script stops the stack, runspg_upgradefrom a temporary PG 15 container, re-enables extensions in a PG 17 container, swaps the data directories while keeping the original in./volumes/db/data.bak.pg15, then restarts Supabase on PG 17. It asks for confirmation at each major step (--yesto skip).Verify, then clean up
Check the version with
docker compose exec db psql -U postgres -c "SELECT version();", thendocker compose ps, and test a full API call. Ifdocker-compose.pg15.ymlis still listed inCOMPOSE_FILE, remove it withsh run.sh config remove pg15. Only once everything is verified, delete./volumes/db/data.bak.pg15,./volumes/db/pgsodium_root.key.bak.pg15and the./volumes/db/pg17_upgrade_bin_*.tar.gzarchive to reclaim space.