Self-hosting Supabase on Ubuntu 24.04 gives you PostgreSQL, Auth, Storage, Realtime, Studio, the REST API, and supporting services on infrastructure you operate yourself. The current supported deployment path is Supabase's version-tracked Docker Compose bundle, not a hand-built collection of unrelated container tags.
Raff Technologies is the VM platform used by the saved test environment. The existing test note remains Ubuntu 24.04 LTS on Raff 2 vCPU / 4 GB RAM VM (existing tested environment; current Supabase minimum). This revision re-verifies the procedure against Supabase's current self-hosting, HTTPS, and update documentation without claiming a fresh end-to-end machine test.
Supabase currently documents 2 CPU cores, 4 GB RAM, and 40 GB SSD as the minimum for the full default stack, with 4 cores, 8 GB+ RAM, and 80 GB+ SSD recommended. The current manual-install documentation shows the stable self-hosted configuration snapshot self-hosted/v0.8.0; the Linux quick-start records whichever current snapshot it installs in .supabase-version. Keep that file: the supported update.sh workflow uses it as the merge base for future updates.
Self-hosting also moves operational responsibility to you. Supabase's update tooling backs up configuration, but it does not back up Postgres or Storage data. This tutorial therefore treats the database, Storage objects, .env, keys, and versioned configuration as one recovery plan.
Prerequisites:
- Ubuntu 24.04 with SSH and sudo access
- At least 2 CPU cores, 4 GB RAM, and 40 GB SSD for the full default stack
- A domain such as
supabase.example.compointing to the VM - Public TCP 80 and 443 available for HTTPS
- A tested recovery path before firewall or upgrade changes
- An off-server destination for backups
Step 1 — Verify Ubuntu, resources, DNS, and existing listeners
Confirm the operating system and architecture:
cat /etc/os-release uname -m
Check CPU, memory, and storage:
nproc free -h df -h / lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS
Set your public Supabase hostname and verify DNS:
export SUPABASE_DOMAIN=supabase.example.com dig +short A "$SUPABASE_DOMAIN" dig +short AAAA "$SUPABASE_DOMAIN"
Only publish an AAAA record if IPv6 really reaches this server and is protected consistently.
Inspect current listeners:
sudo ss -tulpn
Ports 80 and 443 must be available for the HTTPS reverse proxy. Do not continue with the full default stack if the server is below Supabase's documented minimum.
Verify: Ubuntu should report 24.04, the VM should meet the current minimum, DNS should point to this server, and no unexpected service should already own TCP 80 or 443.
Step 2 — Install prerequisites and inspect Supabase's official setup script
Install the utilities used by the current Linux quick-start:
sudo apt update sudo apt install -y ca-certificates curl git openssl jq dnsutils
Download the official setup script to a file instead of piping it directly into a shell:
curl -fsSL https://supabase.link/setup.sh -o setup.sh
Inspect it before execution:
sed -n '1,220p' setup.sh
Supabase documents this script as the Linux quick-start for Debian/Ubuntu and related supported distributions. It can install Docker Engine when needed, fetch the versioned Docker configuration, generate secrets and API keys, prompt for public URLs, record the installed release, and pull the tested image set.
Check the file exists and is not empty:
test -s setup.sh && echo 'Supabase setup script downloaded'
Verify: setup.sh should be present locally and reviewed before you run it; do not execute an uninspected remote shell pipeline as part of a production deployment.
Step 3 — Run the version-tracked Supabase Linux setup
Run the downloaded script:
sh setup.sh
When prompted, use your actual deployment URLs. A common layout is:
SUPABASE_PUBLIC_URL=https://supabase.example.com API_EXTERNAL_URL=https://supabase.example.com/auth/v1 PROXY_DOMAIN=supabase.example.com SITE_URL=https://app.example.com
SITE_URL is the URL of your own frontend application where users land after authentication; it is not necessarily the Supabase host.
The quick-start normally creates supabase-project in the current directory. Enter it and inspect the generated files:
cd supabase-project ls -la cat .supabase-version
Current new installations record their release in .supabase-version. Supabase's current manual-install documentation shows self-hosted/v0.8.0 as the stable snapshot at this revision, but always trust the value actually recorded by the installer you ran.
Verify: The project should contain .env, docker-compose.yml, run.sh, update.sh on current bundles, and .supabase-version; the version file must not be empty.
Step 4 — Review generated secrets, API keys, and URL roles
Display generated credentials only in a private administrator session:
sh run.sh secrets
Review .env locally:
nano .env
Confirm the public URL values:
SUPABASE_PUBLIC_URL=https://supabase.example.com API_EXTERNAL_URL=https://supabase.example.com/auth/v1 SITE_URL=https://app.example.com PROXY_DOMAIN=supabase.example.com
Confirm that a non-default DASHBOARD_PASSWORD exists. Current Supabase setup also generates newer publishable/secret API keys alongside legacy anon/service-role compatibility values.
Never expose server-side secret keys or service-role credentials in browser code, public repositories, screenshots, logs, or support tickets.
Search for obvious placeholders without printing the real secrets:
grep -nE 'CHANGE_ME|your-super-secret|your-tenant-id|your-domain' .env || true
Review every returned line before continuing.
Verify: Public URLs should match the real deployment, the dashboard password should be generated rather than default, and no secret placeholder should remain unresolved.
Step 5 — Protect .env and prepare the firewall without locking out SSH
Restrict the environment file:
chmod 600 .env stat -c '%a %n' .env
Keep a protected off-server copy of .env and any generated signing-key material; these values are part of the recovery plan.
Check UFW before changing firewall state:
sudo ufw status numbered
If UFW is already active, verify the real SSH rule and allow HTTPS traffic:
sudo ufw allow 80/tcp comment 'Supabase HTTP/ACME' sudo ufw allow 443/tcp comment 'Supabase HTTPS' sudo ufw status numbered
If UFW is inactive, do not blindly enable it from a single SSH session. Use the lockout-safe flow in Set Up UFW Firewall on Ubuntu 24.04.
Do not expose internal Postgres, Supavisor, API-gateway, Studio, or service ports to the public internet unless you have an explicit network design for them.
Verify: .env should be mode 600, SSH should remain reachable, and only the public ports intentionally required by the deployment should be exposed.
Step 6 — Start Supabase and verify container health
Start the versioned stack:
cd supabase-project sh run.sh start
Supabase's current helper wraps Docker Compose startup and waits for health checks.
Inspect service state:
docker compose ps
If one or more services do not become healthy, run the bundled diagnostics:
sh tests/test-container-logs.sh
Inspect a specific service when needed:
sh run.sh logs storage
Do not fix a health problem by independently upgrading one random container image. Supabase publishes tested self-hosted snapshots because the services are expected to work together.
Verify: Required containers should be Up and healthy; unresolved restart loops or unhealthy services must be fixed before enabling public traffic.
Step 7 — Enable Supabase's Caddy HTTPS override
Supabase's current production guidance requires HTTPS and provides a preconfigured Caddy override.
Confirm DNS first:
SUPABASE_DOMAIN=supabase.example.com dig +short A "$SUPABASE_DOMAIN"
Enable the Caddy override and start the stack:
cd supabase-project sh run.sh config add caddy sh run.sh start
The bundled proxy terminates TLS in front of the Supabase API gateway and handles WebSocket upgrades required by Realtime.
Check the Caddy service if certificate issuance fails:
docker logs supabase-caddy --tail=100
Verify: Caddy should remain running, obtain a trusted certificate for the real domain, and ports 80/443 should be the intended public ingress path.
Step 8 — Verify HTTPS, Auth, Studio, and the API gateway
Test the public Auth route:
curl -I https://supabase.example.com/auth/v1/
Supabase's HTTPS documentation uses a 401 response here as proof that the request reached Auth through the secure reverse proxy.
Open Studio:
https://supabase.example.com
Sign in with the generated dashboard credentials.
Then test the REST gateway:
curl -I https://supabase.example.com/rest/v1/
An unauthenticated REST request may be rejected; the important check is that the public HTTPS gateway responds and does not require direct access to internal container ports.
In Studio, confirm the Database, Authentication, Storage, and Realtime areas load without service errors.
Verify: HTTPS should be trusted, Auth should respond through the public domain, Studio should accept the generated credentials, and the REST gateway should be reachable over the same HTTPS hostname.
Step 9 — Create a small end-to-end database and API test
Use Studio's SQL editor to create a disposable table:
create table if not exists public.raff_supabase_test ( id bigint generated by default as identity primary key, note text not null, created_at timestamptz not null default now() ); insert into public.raff_supabase_test (note) values ('self-hosted Supabase verification'); select * from public.raff_supabase_test;
This confirms the database is writable through the application stack.
If you test the REST API from a client, use the generated publishable client key or the compatibility anon key according to the current application integration you are validating. Do not put the secret/server-side or service-role credential into a browser test.
After validation, remove the disposable table:
drop table if exists public.raff_supabase_test;
Verify: The row should be created and readable in Studio, and the disposable table should be removed after the test.
Step 10 — Understand Postgres access before exposing any database port
Self-hosted Supabase currently uses Supavisor as the default connection pooler. Supabase documents session mode on port 5432 and transaction mode on port 6543 for different client patterns.
Do not interpret those ports as a recommendation to make them public. For application servers on the same private network, prefer private connectivity and firewall rules that allow only the intended source systems.
Check the compose model before creating any external database rule:
cd supabase-project docker compose config | grep -nE '5432|6543|8000' | head -n 40
If a remote database client is genuinely required, use the connection mode that matches the workload and restrict the source network explicitly. For general internet-facing application traffic, use the HTTPS API gateway rather than exposing Postgres broadly.
Verify: There should be no accidental public database exposure; any 5432/6543 access should be intentional, source-restricted, and tied to a known client requirement.
Step 11 — Back up Postgres, Storage objects, configuration, and version metadata
Supabase's update.sh backs up configuration files, but the current documentation explicitly states that it does not back up Postgres or Storage data. Create your own recovery set before upgrades and before important production changes.
Create a protected backup directory:
cd supabase-project mkdir -p backups/manual chmod 700 backups/manual STAMP="$(date -u +%Y%m%d-%H%M%S)"
For a logically recoverable database copy, preserve roles and the main Postgres database separately:
docker compose exec -T db \ pg_dumpall -U postgres --roles-only \ | gzip > "backups/manual/roles-${STAMP}.sql.gz" docker compose exec -T db \ pg_dump -U postgres -d postgres -Fc \ > "backups/manual/postgres-${STAMP}.dump"
If Storage is in use, coordinate a maintenance window or otherwise stop application writes while the database dump and Storage copy are taken so that database metadata and object files do not drift apart.
Archive the self-hosted Storage data directory used by the official Docker bundle:
tar -czf "backups/manual/storage-${STAMP}.tar.gz" \ volumes/storage
Archive the sensitive configuration and recorded release:
tar -czf "backups/manual/config-${STAMP}.tar.gz" \ .env .supabase-version docker-compose.yml run.sh update.sh volumes/proxy 2>/dev/null || \ tar -czf "backups/manual/config-${STAMP}.tar.gz" \ .env .supabase-version docker-compose.yml run.sh volumes/proxy chmod 600 backups/manual/*
Validate the artifacts:
gzip -t "backups/manual/roles-${STAMP}.sql.gz" pg_restore -l "backups/manual/postgres-${STAMP}.dump" | head tar -tzf "backups/manual/storage-${STAMP}.tar.gz" | head tar -tzf "backups/manual/config-${STAMP}.tar.gz" | head
Copy the complete recovery set off the VM. Raff Object Storage can be an off-server target for S3-compatible backup tooling, while Data Protection can provide an additional VM-level recovery layer.
