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

How to Self-Host Supabase on VPS with Docker

Run Supabase Docker stack for your own server. Replace the published demo secrets, understand all 14 services, check RAM needs, backups, and safe updates.

Wetin you dey build

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

The install short. Na the .env file dey usually cause wahala. 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 one VPS first. Everything for here assume say docker compose version already dey print one version.

Wetin dey inside the stack

Supabase no be one program. The Compose file dey start separate services for one network. If you know which one be which, container names no go look like one wall wey hard to debug.

  • db na PostgreSQL with Supabase extensions wey don load. Every other service dey talk to am. If this container no healthy, everything else go fail too.
  • kong na the API gateway. E dey listen for port 8000 and route /rest/v1/, /auth/v1/ and /storage/v1/ go the correct backend. Na the only container wey you suppose ever expose.
  • rest na PostgREST. E dey read your Postgres schema and serve am as REST API. So when you create new table, e become new endpoint without 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 wey dey behind am.
  • analytics (Logflare) and vector dey collect logs, while supavisor na the Postgres connection pooler.

Na why the resource numbers below be the way dem be. You no dey run only database. You dey run database plus about twelve support services.

Sizing: plan say you need 8 GB of RAM

For fresh install, the stack dey use roughly 2.5 to 3 GB resident memory as of July 2026, before your own data or traffic enter. The analytics service and the Studio Node.js process na the two biggest users by themselves. 2 GB server go start the containers, then kernel out of memory killer go kill one, usually analytics or db. The sign na container wey dey restart continuously with exit code 137.

Give am 8 GB of RAM and 4 vCPU for anything wey you depend on. 4 GB fit work for one-person development instance if you accept say heavy query and Studio session wey dey run together go slow. Disk matter too, because Postgres, storage volume, and log data all dey under the project directory. Start with 40 GB and monitor am. Make you count the services before you choose a plan. Na good habit for anything wey you self-host, because PhotoPrism and Immich get real RAM minimum wey pass wetin their quick start pages dey suggest.

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. Some of these values dey write into data during first boot, so if you change dem later, you go need reset the database.

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

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 and the Logflare tokens into .env. E need openssl, wey dey present for any normal Ubuntu image.

Two values e no set, and you must edit dem by hand for .env:

  • POSTGRES_PASSWORD. Use letters and digits only. Punctuation here dey break the connection strings wey several services build by joining strings. The failure go look like authentication error instead of parsing error, so people go dey investigate wrong place.
  • DASHBOARD_USERNAME and DASHBOARD_PASSWORD. These na the basic authentication credentials for Studio. The default password wey ship with am na literally this_password_is_insecure_and_should_be_updated.

Understand why ANON_KEY and SERVICE_ROLE_KEY no fit be invented. Both na JWTs wey get signature with JWT_SECRET. The gateway dey verify that signature for every request, so e go reject key wey no match your secret with {"message":"Invalid authentication credentials"}. This na the most common self-hosting failure: operator change JWT_SECRET but keep the demo keys. Generate all three together, always.

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

Set SITE_URL and API_EXTERNAL_URL to the address wey your users go actually reach, for example https://supabase.example.com. Auth dey build email confirmation and OAuth callback links from those values, so if you leave dem at http://localhost:8000, every user go get 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

Kong for port 8000 dey speak plain HTTP. Every API key and every user password dey cross network as clear text, and 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, and bind Kong to loopback address so nothing else fit reach am. For docker-compose.yml, the kong port mapping go become 127.0.0.1:8000:8000, and the proxy go forward traffic go there. Traefik for front of several Compose apps cover the certificate side.

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

Backup database, no be the directory

Postgres data dey for bind mount at ./volumes/db/data. If you copy that directory while the container still dey run, you go get incomplete copy, 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 go outside the server. Na wetin encrypted offsite backups with restic dey handle. Back up your .env for the same time. If you lose JWT_SECRET, every token wey you don issue go become invalid, and every encrypted secret wey you store no go readable again.

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

Update wey no go lose data

Supabase dey pin image versions for docker-compose.yml, so nothing go change until you change am. E good make you copy this pinning approach 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. 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 the bind mounts for the host, not inside the containers. Read CHANGELOG.md for the repository before you jump major version, because Postgres major upgrades no dey automatic and dem need dump plus restore.

To collect changes wey happen for the Compose file itself, clone the upstream repository again and copy its docker directory over your project. Make sure say you no overwrite .env.

The complete reset dey for another script. E go destroy everything, including the database, and e go ask you to confirm:

sh reset.sh

FAQ

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

Your ANON_KEY or SERVICE_ROLE_KEY no get signature from the JWT_SECRET wey dey currently inside .env. The gateway dey verify signature for every request and reject am if e no match. Regenerate all three together with sh utils/generate-keys.sh --update-env, then run sh run.sh recreate make the services read the new values.

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

E no reliable. The stack dey use almost 3 GB as of July 2026 because e dey run about fourteen services. Because of this, 2 GB box dey lose containers to 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 dey send the address wey you tell am to send. Set both to your real public URL, then recreate the stack.