Supabase Self-Hosted on Ubuntu (Docker)
When to use
Use when user asks to install/run Supabase via Docker on Ubuntu server and needs DB connection for a website/backend.
Why this skill exists
Ubuntu package names for Docker Compose differ across releases. docker-compose-plugin may be unavailable even though Compose v2 is available as docker-compose-v2.
Procedure
Prerequisites check
docker --version
docker compose version (or fallback docker-compose version)
ss -lntp to check port conflicts (8000, 8443, 5432, 6543)
Install required packages
- Install git
- Install Compose v2 on Ubuntu if missing:
apt-get install -y docker-compose-v2
- Optional legacy CLI (not required):
apt-get install -y docker-compose
Get Supabase self-hosted repo
git clone https://github.com/supabase/supabase.git /opt/supabase
- Use background execution if clone can exceed foreground timeout.
Initialize Docker config
cd /opt/supabase/docker
cp -n .env.example .env
- Generate secrets safely:
sh ./utils/generate-keys.sh --update-env
Set deployment-specific env values
Update .env minimally:
SUPABASE_PUBLIC_URL=http://<host>:8000
API_EXTERNAL_URL=http://<host>:8000
SITE_URL=https://<host> (or appropriate app URL)
POOLER_TENANT_ID=<tenant-id> (required for pooler username format)
Start stack
docker compose up -d
- First run pulls many large images; can take several minutes.
Verify health
docker compose ps
- Ensure core services are
running/healthy:
supabase-db, supabase-auth, supabase-rest, supabase-storage, supabase-meta, supabase-kong, supabase-pooler, supabase-studio, realtime, analytics
- Verify host listening ports:
App DB connection outputs
From .env:
POSTGRES_PASSWORD
POSTGRES_PORT (usually 5432)
POOLER_PROXY_PORT_TRANSACTION (usually 6543)
POOLER_TENANT_ID
Typical connection strings:
Session mode (recommended default)
postgresql://postgres:<POSTGRES_PASSWORD>@<host>:5432/postgres
Transaction mode (pooler)
postgresql://postgres.<POOLER_TENANT_ID>:<POSTGRES_PASSWORD>@<host>:6543/postgres
Vercel integration (when backend is deployed on Vercel)
If the app runs on Vercel (e.g. Next.js API routes), update project env vars right after Supabase is live:
NEXT_PUBLIC_SUPABASE_URL = http://<host>:8000 (or HTTPS URL if TLS fronted)
NEXT_PUBLIC_SUPABASE_ANON_KEY = ANON_KEY from /opt/supabase/docker/.env
SUPABASE_SERVICE_ROLE_KEY = SERVICE_ROLE_KEY from /opt/supabase/docker/.env
Recommended commands (non-interactive):
npx --yes vercel link --yes --project <project> --cwd <repo> --scope <team> --token <token>
npx --yes vercel env add <NAME> production --value '<value>' --yes --force ...
npx --yes vercel env add <NAME> preview '' --value '<value>' --yes --force ...
npx --yes vercel deploy --prod --yes ...
Vercel DNS commands (team-scoped):
- List records:
npx --yes vercel dns ls <domain> --scope <team> --token <token>
- Add A record:
npx --yes vercel dns add <domain> <name> A <ip> --scope <team> --token <token>
- Example:
vercel dns add faftech.net database A 103.208.206.199 ...
- Common pitfall:
vercel dns add argument order is <domain> <name> <type> <value> (not <fqdn> <value> <type>).
- Common pitfall: forgetting
--scope <team> can produce permission errors even with valid token.
Why preview '' matters:
- Vercel CLI may prompt for git branch when setting Preview env. Passing empty positional git-branch (
'') avoids interactive prompt and applies to all preview branches.
Common pitfalls
docker-compose-plugin not found on Ubuntu Jammy: install docker-compose-v2.
- First
docker compose up -d may appear stalled; it is usually pulling/extracting large layers.
vercel env pull can show sensitive values redacted/empty; verify presence with vercel env ls <environment> instead of assuming missing values.
- Do not expose raw secrets in chat logs; read from
.env only when needed.
- If user wants production hardening, additional TLS/reverse proxy and firewall setup is required.
- Critical for Vercel/external backends: ensure
SUPABASE_PUBLIC_URL hostname resolves to a publicly reachable IP, not private overlay IPs (e.g. 100.x.x.x Tailscale/CGNAT). If DNS points to private IP, Vercel will timeout and API routes may return app-level NOT_FOUND fallbacks.
- After DNS changes, verify from outside the host (not only localhost/server shell):
dig +short <host> from public resolver (@8.8.8.8)
curl from external context to http://<host>:8000/rest/v1/ (expect 401 without key, not timeout)
- Only then redeploy app on Vercel.
- If frontend shows empty data after env update, check whether DB was actually migrated/seeded. Validate row counts directly in
supabase-db (psql) before blaming app code.
- In some Next.js backends, API handlers catch all exceptions and return
notFoundResponse(...) (404). This can mask real connectivity errors (ECONNREFUSED, ENOTFOUND) as fake "data not found". Always verify upstream connectivity separately.
- For Vercel debugging, bypass cache when testing API routes (
?t=<timestamp> + cache: no-store) and inspect x-vercel-cache (MISS/HIT/PRERENDER) to avoid misreading stale responses.
- If service is reachable on
127.0.0.1, LAN IP, or Tailscale IP but refused on public domain/IP, root cause is edge exposure (router/NAT/ISP/tunnel), not Supabase/container internals.
- If user refuses port forwarding, prepare alternatives explicitly:
- Cloudflare Tunnel (requires Cloudflare token/zone control), or
- Tailscale Funnel (must be enabled in tailnet admin first; otherwise
Funnel is not enabled on your tailnet).
Cloudflare Tunnel + external DNS provider (critical gotcha)
When authoritative DNS is not on Cloudflare (for example NS is on Vercel), tunnel routing needs Partial/CNAME setup behavior.
Symptoms of wrong setup
cloudflared service shows healthy connections, but public URL still timeout.
- DNS CNAME points directly to
<tunnel-id>.cfargotunnel.com.
dig may return only AAAA for *.cfargotunnel.com, while external clients time out.
Correct pattern for partial/CNAME setup
- Ensure
faftech.net (or your zone) exists in Cloudflare account as a zone (partial/CNAME or full).
- Add published route in tunnel for
database.<domain> -> http://127.0.0.1:8000.
- At external DNS provider (e.g. Vercel DNS), set:
database.<domain> CNAME database.<domain>.cdn.cloudflare.net
- Do not point external DNS directly to
<tunnel-id>.cfargotunnel.com for partial setup.
Required token permissions to fully automate
Cloudflare One Connector: cloudflared → Edit
Cloudflare One Connectors → Edit
Argo Tunnel (Legacy) → Edit (compat)
Account Zone Create (needed if zone not yet created)
Zone DNS Edit (only if automating DNS in Cloudflare zone)
Fast blocker checks
GET /accounts/{account_id}/cfd_tunnel success + status healthy
GET /zones?name=<domain> returns at least one zone
- If zone list empty and cannot create zone (
com.cloudflare.api.account.zone.create missing), tunnel cannot serve custom domain end-to-end yet
Communication rule (user-experience)
If tunnel agent is healthy but zone is missing, state blocker explicitly as the single root cause; do not keep cycling app redeploys.
Reachability triage matrix (fast)
Use this exact order before changing app code:
curl http://127.0.0.1:8000/rest/v1/ on host (expect 401 without key)
curl http://<LAN_IP>:8000/rest/v1/ from host/LAN
curl http://<Tailscale_IP>:8000/rest/v1/ if tailscale used
dig +short <public-subdomain> @8.8.8.8
- External/public check to
http://<public-subdomain>:8000/rest/v1/
Interpretation:
- Steps 1–3 pass, 5 fails ⇒ external path issue (NAT/firewall/tunnel), not DB/migration.
- Step 4 fails ⇒ DNS issue.
- Step 1 fails ⇒ local container/service issue.
Quick verification checklist
docker compose version works
.env exists and secrets are generated (not default placeholders)
docker compose ps shows healthy services
- host ports are bound (
ss -lntp)
- application receives valid DATABASE_URL
1---2name: supabase-self-hosted-docker-ubuntu3description: Deploy Supabase self-hosted on Ubuntu with Docker, handle Compose package quirks, initialize secure .env, bring up stack, and verify DB connectivity for app use.4license: MIT5---67# Supabase Self-Hosted on Ubuntu (Docker)89## When to use10Use when user asks to install/run Supabase via Docker on Ubuntu server and needs DB connection for a website/backend.1112## Why this skill exists13Ubuntu package names for Docker Compose differ across releases. `docker-compose-plugin` may be unavailable even though Compose v2 is available as `docker-compose-v2`.1415## Procedure16171. **Prerequisites check**18 - `docker --version`19 - `docker compose version` (or fallback `docker-compose version`)20 - `ss -lntp` to check port conflicts (8000, 8443, 5432, 6543)21222. **Install required packages**23 - Install git24 - Install Compose v2 on Ubuntu if missing:25 - `apt-get install -y docker-compose-v2`26 - Optional legacy CLI (not required):27 - `apt-get install -y docker-compose`28293. **Get Supabase self-hosted repo**30 - `git clone https://github.com/supabase/supabase.git /opt/supabase`31 - Use background execution if clone can exceed foreground timeout.32334. **Initialize Docker config**34 - `cd /opt/supabase/docker`35 - `cp -n .env.example .env`36 - Generate secrets safely:37 - `sh ./utils/generate-keys.sh --update-env`38395. **Set deployment-specific env values**40 Update `.env` minimally:41 - `SUPABASE_PUBLIC_URL=http://<host>:8000`42 - `API_EXTERNAL_URL=http://<host>:8000`43 - `SITE_URL=https://<host>` (or appropriate app URL)44 - `POOLER_TENANT_ID=<tenant-id>` (required for pooler username format)45466. **Start stack**47 - `docker compose up -d`48 - First run pulls many large images; can take several minutes.49507. **Verify health**51 - `docker compose ps`52 - Ensure core services are `running/healthy`:53 - `supabase-db`, `supabase-auth`, `supabase-rest`, `supabase-storage`, `supabase-meta`, `supabase-kong`, `supabase-pooler`, `supabase-studio`, `realtime`, `analytics`54 - Verify host listening ports:55 - `8000`, `8443`, `5432`, `6543`5657## App DB connection outputs58From `.env`:59- `POSTGRES_PASSWORD`60- `POSTGRES_PORT` (usually 5432)61- `POOLER_PROXY_PORT_TRANSACTION` (usually 6543)62- `POOLER_TENANT_ID`6364Typical connection strings:6566### Session mode (recommended default)67`postgresql://postgres:<POSTGRES_PASSWORD>@<host>:5432/postgres`6869### Transaction mode (pooler)70`postgresql://postgres.<POOLER_TENANT_ID>:<POSTGRES_PASSWORD>@<host>:6543/postgres`7172## Vercel integration (when backend is deployed on Vercel)73If the app runs on Vercel (e.g. Next.js API routes), update project env vars right after Supabase is live:7475- `NEXT_PUBLIC_SUPABASE_URL` = `http://<host>:8000` (or HTTPS URL if TLS fronted)76- `NEXT_PUBLIC_SUPABASE_ANON_KEY` = `ANON_KEY` from `/opt/supabase/docker/.env`77- `SUPABASE_SERVICE_ROLE_KEY` = `SERVICE_ROLE_KEY` from `/opt/supabase/docker/.env`7879Recommended commands (non-interactive):80- `npx --yes vercel link --yes --project <project> --cwd <repo> --scope <team> --token <token>`81- `npx --yes vercel env add <NAME> production --value '<value>' --yes --force ...`82- `npx --yes vercel env add <NAME> preview '' --value '<value>' --yes --force ...`83- `npx --yes vercel deploy --prod --yes ...`8485Vercel DNS commands (team-scoped):86- List records: `npx --yes vercel dns ls <domain> --scope <team> --token <token>`87- Add A record: `npx --yes vercel dns add <domain> <name> A <ip> --scope <team> --token <token>`88 - Example: `vercel dns add faftech.net database A 103.208.206.199 ...`89- Common pitfall: `vercel dns add` argument order is `<domain> <name> <type> <value>` (not `<fqdn> <value> <type>`).90- Common pitfall: forgetting `--scope <team>` can produce permission errors even with valid token.9192Why `preview ''` matters:93- Vercel CLI may prompt for git branch when setting Preview env. Passing empty positional git-branch (`''`) avoids interactive prompt and applies to all preview branches.9495## Common pitfalls96- `docker-compose-plugin` not found on Ubuntu Jammy: install `docker-compose-v2`.97- First `docker compose up -d` may appear stalled; it is usually pulling/extracting large layers.98- `vercel env pull` can show sensitive values redacted/empty; verify presence with `vercel env ls <environment>` instead of assuming missing values.99- Do not expose raw secrets in chat logs; read from `.env` only when needed.100- If user wants production hardening, additional TLS/reverse proxy and firewall setup is required.101- **Critical for Vercel/external backends:** ensure `SUPABASE_PUBLIC_URL` hostname resolves to a **publicly reachable IP**, not private overlay IPs (e.g. `100.x.x.x` Tailscale/CGNAT). If DNS points to private IP, Vercel will timeout and API routes may return app-level `NOT_FOUND` fallbacks.102- After DNS changes, verify from outside the host (not only localhost/server shell):103 - `dig +short <host>` from public resolver (`@8.8.8.8`)104 - `curl` from external context to `http://<host>:8000/rest/v1/` (expect 401 without key, not timeout)105 - Only then redeploy app on Vercel.106- If frontend shows empty data after env update, check whether DB was actually migrated/seeded. Validate row counts directly in `supabase-db` (`psql`) before blaming app code.107- In some Next.js backends, API handlers catch all exceptions and return `notFoundResponse(...)` (404). This can mask real connectivity errors (`ECONNREFUSED`, `ENOTFOUND`) as fake "data not found". Always verify upstream connectivity separately.108- For Vercel debugging, bypass cache when testing API routes (`?t=<timestamp>` + `cache: no-store`) and inspect `x-vercel-cache` (`MISS/HIT/PRERENDER`) to avoid misreading stale responses.109- If service is reachable on `127.0.0.1`, LAN IP, or Tailscale IP but refused on public domain/IP, root cause is edge exposure (router/NAT/ISP/tunnel), not Supabase/container internals.110- If user refuses port forwarding, prepare alternatives explicitly:111 - Cloudflare Tunnel (requires Cloudflare token/zone control), or112 - Tailscale Funnel (must be enabled in tailnet admin first; otherwise `Funnel is not enabled on your tailnet`).113114## Cloudflare Tunnel + external DNS provider (critical gotcha)115When authoritative DNS is **not** on Cloudflare (for example NS is on Vercel), tunnel routing needs **Partial/CNAME setup** behavior.116117### Symptoms of wrong setup118- `cloudflared` service shows healthy connections, but public URL still timeout.119- DNS CNAME points directly to `<tunnel-id>.cfargotunnel.com`.120- `dig` may return only AAAA for `*.cfargotunnel.com`, while external clients time out.121122### Correct pattern for partial/CNAME setup1231. Ensure `faftech.net` (or your zone) exists in Cloudflare account as a zone (partial/CNAME or full).1242. Add published route in tunnel for `database.<domain>` -> `http://127.0.0.1:8000`.1253. At external DNS provider (e.g. Vercel DNS), set:126 - `database.<domain> CNAME database.<domain>.cdn.cloudflare.net`127 - **Do not** point external DNS directly to `<tunnel-id>.cfargotunnel.com` for partial setup.128129### Required token permissions to fully automate130- `Cloudflare One Connector: cloudflared` → Edit131- `Cloudflare One Connectors` → Edit132- `Argo Tunnel (Legacy)` → Edit (compat)133- `Account Zone Create` (needed if zone not yet created)134- `Zone DNS Edit` (only if automating DNS in Cloudflare zone)135136### Fast blocker checks137- `GET /accounts/{account_id}/cfd_tunnel` success + status healthy138- `GET /zones?name=<domain>` returns at least one zone139- If zone list empty and cannot create zone (`com.cloudflare.api.account.zone.create` missing), tunnel cannot serve custom domain end-to-end yet140141### Communication rule (user-experience)142If tunnel agent is healthy but zone is missing, state blocker explicitly as the single root cause; do not keep cycling app redeploys.143144## Reachability triage matrix (fast)145Use this exact order before changing app code:1461. `curl http://127.0.0.1:8000/rest/v1/` on host (expect 401 without key)1472. `curl http://<LAN_IP>:8000/rest/v1/` from host/LAN1483. `curl http://<Tailscale_IP>:8000/rest/v1/` if tailscale used1494. `dig +short <public-subdomain> @8.8.8.8`1505. External/public check to `http://<public-subdomain>:8000/rest/v1/`151152Interpretation:153- Steps 1–3 pass, 5 fails ⇒ external path issue (NAT/firewall/tunnel), not DB/migration.154- Step 4 fails ⇒ DNS issue.155- Step 1 fails ⇒ local container/service issue.156157## Quick verification checklist158- `docker compose version` works159- `.env` exists and secrets are generated (not default placeholders)160- `docker compose ps` shows healthy services161- host ports are bound (`ss -lntp`)162- application receives valid DATABASE_URL