SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-10-21

How to Self-Host Supabase for VPS with Docker

Run Supabase Docker stack for your VPS: replace demo secrets, understand all 11 services, plan RAM, back up Postgres, and update without deleting your database.

Wetin you dey build

Self-hosting Supabase mean say you dey run the official Docker Compose stack for your own server: Postgres, REST API wey dey front am, auth service, file storage, realtime websockets, and Studio dashboard. You go clone one repository, edit one .env file, then start eleven containers wey together behave like Supabase project wey you control.

The install short. Na the .env file dey usually cause problem. E come with demo secrets wey dem publish for the repository, and stack wey start with those defaults open to anybody wey find am. This guide explain the secrets wey you must replace, wetin each service dey do, how much memory the stack really need, and how to update am without deleting your database.

If Compose still new to you, read Docker Compose basics for VPS first. Everything below assume say docker compose version already dey print version.

Stack actually get wetin inside

Supabase no be one program. The Compose file dey start different services for one network. If you know wetin each one dey do, container names no go look like one confusing wall again when you dey debug.

  • db na PostgreSQL with Supabase extensions loaded. Every other service dey talk to am. If this container no healthy, everything else go fail too.
  • api-gw na the API gateway. Since the August 2026 release, e dey run Envoy. Before that, na service called kong dey run there, and Kong still dey available as opt-in override. E dey listen on port 8000 and route /rest/v1/, /auth/v1/ and /storage/v1/ go the correct backend. Na only this container you suppose ever expose.
  • rest na PostgREST. E dey read your Postgres schema and serve am as REST API. So new table fit become new endpoint without writing code.
  • auth na GoTrue. E dey issue the JSON web tokens (JWT) wey identify your users.
  • storage and imgproxy dey handle file uploads and image resizing.
  • realtime dey stream database changes through websockets.
  • studio and meta na the dashboard and the admin API behind am.
  • functions na the Deno based edge runtime, while supavisor na the Postgres connection pooler.

Logs and analytics (Logflare plus vector) comot from the default Compose file for June 2026. Now dem dey inside an opt-in override file wey you enable with sh run.sh config add logs. Na the same Compose file layering wey running one stack from several Compose files describe.

Na why the resource numbers below be like this. You no dey run only database. You dey run database plus ten support services.

Plan size: make provision for 8 GB of RAM

The stack use about 2.5 to 3 GB of resident memory when e idle for fresh installation as of July 2026, before your own data or traffic enter. For that measurement, the analytics service and the Studio Node.js process na the two biggest individual memory users. Analytics no longer dey inside the default stack, so e go only use memory if you turn on the logs override. A 2 GB server go start the containers, then the kernel out of memory killer go kill one. The sign na container wey dey restart continuously with exit code 137. Supabase own self-hosting docs, wey we check for October 2026, list 4 GB of RAM, 2 CPU cores and 40 GB of SSD as the minimum. Dem recommend 8 GB, 4 cores and 80 GB.

Give anything wey you depend on 8 GB of RAM and 4 vCPU. 4 GB fit work for one-person development instance if you accept say heavy query and Studio session wey happen together go slow. Disk matter too, because Postgres, the storage volume and, if you enable logs, the log data all dey under the project directory. Start with 40 GB and monitor am. Count services before you choose plan; na good habit for anything wey you self-host, because PhotoPrism and Immich get real RAM minimums too. Immich quick start page, wey we check for October 2026, ask for at least 6 GB of RAM and 2 CPU cores. If that one pass wetin your project need for now, compare am with wetin Supabase free cloud plan really include, because small app wey dey below 500 MB and no dey idle for one week fit run there without cost.

Install: clone the official repository

The supported path dey copy the docker directory from the main repository go your own project directory. This separation matter because e mean say later git pull no fit overwrite your .env.

git clone --depth 1 https://github.com/supabase/supabase
mkdir supabase-project
cp -rf supabase/docker/* supabase-project
cp supabase/docker/.env.example supabase-project/.env
cd supabase-project
docker compose pull

docker compose pull dey download several gigabytes of images. E suppose end with every service marked Pulled. If manifest unknown error happen here, e mean say dem remove the pinned image tag upstream. The fix na to pull newer copy of the repository, instead of editing tags by hand.

Secrets wey you must change before the first start

Do this before you start the stack, no be after. Several of these values dey write into data for first boot, so if you change dem later, e mean say you must reset the database.

The repository get generator wey dey produce every value correctly, including the two API keys wey must use your new JWT secret sign.

sh utils/generate-keys.sh --update-env

That script dey write new values for JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, SECRET_KEY_BASE, REALTIME_DB_ENC_KEY, VAULT_ENC_KEY, PG_META_CRYPTO_KEY, the Logflare tokens, the S3 protocol keys and MINIO_ROOT_PASSWORD into .env. As of October 2026, e also dey write random values for POSTGRES_PASSWORD and DASHBOARD_PASSWORD. E need openssl, wey dey present for any normal Ubuntu image. Because e now dey rewrite the database password too, run am with --update-env only before the first start. Never run am against stack wey already get data.

Two values still worth checking by hand for .env:

  • POSTGRES_PASSWORD. The script dey fill am with random hex, and this one safe. If you replace am with your own value, use letters and digits only. Punctuation for here dey break the connection strings wey several services dey build by joining strings. The failure go look like authentication error instead of parsing error, so people fit dey search for the wrong problem.
  • DASHBOARD_USERNAME and DASHBOARD_PASSWORD. These na the basic authentication credentials for Studio. The script dey set random password but e dey leave the username as default supabase. The default password wey ship with am na literally this_password_is_insecure_and_should_be_updated, so if you skip the script, change am now.

Understand why ANON_KEY and SERVICE_ROLE_KEY no fit be invented. Both na JWTs wey JWT_SECRET sign. The gateway dey accept only the exact keys inside .env, and the services behind am dey verify the signature against JWT_SECRET. So, if key no match, dem go reject am with HTTP 401. The default Envoy gateway dey return 401 Unauthorized. The current Kong override dey return the same thing; e dey run Kong 3.9 and e dey return {"message":"Unauthorized"}. Installs from before March 2026 dey run Kong 2.8, wey dey answer {"message":"Invalid authentication credentials"}. This na the most common self-hosting failure: operator change JWT_SECRET but e keep the demo keys. Generate all three together, every time.

Treat SERVICE_ROLE_KEY like root password. E dey bypass row level security completely. E belong for server side code and nowhere else.

Set SITE_URL to the address of your app. Set API_EXTERNAL_URL to the public address of the API, with /auth/v1 for the end, for example https://supabase.example.com/auth/v1. Set SUPABASE_PUBLIC_URL to the same public address without the path. Auth dey build email confirmation and OAuth callback links from these values. So if you leave dem as their localhost defaults, every user go dey sent to their own machine.

Then check wetin you get:

sh run.sh secrets

Start am and confirm say e dey healthy

sh run.sh start
docker compose ps

run.sh start wrap docker compose up -d --wait, so e no go return until health checks pass. Every service suppose show running (healthy) or running. First boot dey take two to four minutes because Postgres dey run its initialisation scripts before anything fit connect.

If container dey restart, read its logs with service name:

docker compose logs db
docker compose logs auth

Studio go dey for port 8000, and e go ask for the dashboard username and password wey you set.

No put port 8000 for public internet

Gateway wey dey for 8000 dey speak plain HTTP. Every API key and every user password dey cross the network as clear text. The Studio credentials na basic authentication, wey be base64 encoding instead of encryption.

Put reverse proxy for front of am. Terminate TLS (transport layer security) there, then bind gateway to loopback address so nothing else fit reach am. For docker-compose.yml, the port line under api-gw service go become 127.0.0.1:${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp, and proxy go forward request go there. For installation wey happen before August 2026 switch to Envoy, the same port line dey under service wey dem call kong instead. Traefik for front of several Compose apps explain the certificate side. The same proxy go end up for front of everything else for the server, from this stack reach something as unserious as a Jellyfin library wey dem rebuild as 90s video store, and all of dem need hostname instead of another open port. If na dashboard wey na only you go ever use, skip the proxy and reach the loopback port through SSH tunnel instead. Na the same approach self-hosted open-kritt use to keep its scanning interface completely off public internet.

Close the remaining ports for firewall too, because Docker dey publish ports by writing its own iptables rules wey simple ufw configuration no dey see. why Docker containers ignore your ufw rules explain this trap.

Database back up, no be directory

Postgres data dey inside bind mount for ./volumes/db/data. If you copy that directory while the container still dey run, the copy fit no complete properly, because Postgres dey buffer writes and the files for disk only dey consistent when checkpoint happen. Restoring am usually go work, but sometimes e go quietly lose the last transactions. This na the worst kind failure wey backup fit get.

Use dump instead. pg_dumpall dey run inside the container and e dey produce consistent snapshot:

docker exec -t supabase-db pg_dumpall -U postgres > supabase-$(date +%F).sql

Check say the file no empty before you trust am. Then schedule make you send those dumps comot from the server. Na wetin encrypted offsite backups with restic dey do be that. If scheduled dump fail quietly, e no different from having no backup at all. So make the cron or systemd job send alert go your phone when e exits non-zero. Back up your .env for the same time. If you lose JWT_SECRET, every token wey you issue go become invalid, and every encrypted secret wey you store go become unreadable.

Uploaded files dey inside ./volumes/storage. Dem na ordinary files, so plain copy dey okay.

Update without losing data

Supabase dey pin image versions for docker-compose.yml, so nothing go change until you change am. Pinning dey worth copying for any stack wey you assemble by hand. Na why self-hosted RustDesk relay dey pin its two server images instead of following moving tag. Upgrade suppose be something wey you choose for morning wey you get time for am. Make you take dump first, every time.

docker compose pull
sh run.sh recreate

recreate dey stop the stack and start am again with the new images. Your data dey safe because e dey inside bind mounts for the host, not inside the containers. Read CHANGELOG.md for the repository before you jump major version, because Postgres major upgrade na separate step wey recreate no dey perform. Postgres 17 don dey default since June 2026. Existing install for Postgres 15 go move to am with sudo bash utils/upgrade-pg17.sh. E dey use pg_upgrade to migrate the data in place and ask for confirmation for each major step. All containers must dey run, and free disk must be at least twice your database size plus 5 GB. Supabase still recommend make you get your own independent backup before you start.

To apply changes wey you make to the Compose file itself, run sh update.sh from the project directory. Or run sh update.sh --dry-run to first see wetin go change. E merge the upstream files with your own edits. E never overwrite existing values for .env; e only append new keys. E archive your configuration under backups/ before e write am, but e no archive the database. Install wey no get .supabase-version file need --from and the release wey e start from. This one important especially for the August 2026 release. That release replace the kong service with api-gw and require docker-compose.yml update. The manual method still dey work: clone the upstream repository again and copy its docker directory over your project. Take care make you no overwrite .env.

The full reset dey destroy everything, including the database, and return .env to the default values. The old one dey kept as .env.old. E na separate script, and e go ask for confirmation:

sh reset.sh

FAQ

Why my API calls dey return "Invalid authentication credentials"?

Na Kong 2.8 dey return that exact message, the gateway wey dey inside stack for installations from before March 2026. The default Envoy gateway and the current Kong override, wey dey run Kong 3.9, report the same problem as plain 401 Unauthorized. For either case, the key wey you send no match ANON_KEY or SERVICE_ROLE_KEY for .env, or dem keys no dey signed with JWT_SECRET wey dey inside .env. For stack wey never hold data before, regenerate all three together with sh utils/generate-keys.sh --update-env. For stack wey dey run, run the script without the flag and answer no, so e go only print new values. Copy JWT_SECRET, ANON_KEY and SERVICE_ROLE_KEY enter .env by hand, and leave POSTGRES_PASSWORD as e be. Then run sh run.sh recreate so the services go read the new values.

I fit run self-hosted Supabase for 2 GB VPS?

No be reliably. Supabase own self-hosting docs list 4 GB RAM as the minimum as of October 2026. The stack dey idle near 3 GB as of July 2026 for measurement wey include the logging services, wey now dey opt-in. A 2 GB box fit lose containers to the out of memory killer, and you go see exit code 137 for docker compose ps. Use 8 GB for production, and treat 4 GB as the minimum for solo development.

Self-hosted Supabase include edge functions?

Yes. The Compose file include the Deno-based functions runtime, and e dey serve anything wey you put under ./volumes/functions. E no include the hosted platform global deployment network, so your functions go run for your one server, for one location.

How I fit connect directly to the Postgres database?

Use docker exec -it supabase-db psql -U postgres for interactive shell on the server itself. For external client, connect through Supavisor on port 5432 with user postgres.<POOLER_TENANT_ID> and your POSTGRES_PASSWORD. No open that port to internet. Reach am through VPN or SSH tunnel.

SITE_URL and API_EXTERNAL_URL for .env remain with their default values. The auth service dey build every confirmation and password reset link from those two values, so e send the address wey dem tell am to send. Set both to your real public URL and recreate the stack.

Dis article help you?