Skip to content
All notes

Self-hosting Supabase on Oracle Cloud's free tier

How we run isolated Supabase projects side by side on one Always Free Arm server — and the guardrails that keep it safe.

  • Supabase
  • Oracle Cloud
  • Docker
  • Security

Supabase Cloud is excellent, but free projects are limited and paid plans add up quickly for a pre-revenue studio. Supabase is open source and ships an official Docker setup, so we run it on an Oracle Cloud Always Free Arm instance. This note covers the design and the guardrails, not a step-by-step install.

One script = one "New project" button

We wanted the self-hosted equivalent of clicking New project: a fully isolated Postgres, Auth, REST, Realtime and Functions stack per project. A single script, new-project.sh, does that:

  • copies a pinned upstream docker/ folder into its own project directory — the upstream compose file is never edited
  • generates fresh secrets into a .env file with restricted permissions; secrets are never printed
  • writes a docker-compose.override.yml with unique container names, memory caps and smaller Postgres settings
  • assigns ports from a slot number, so the API gateway and the session and transaction poolers of each project never collide
  • validates the merged config before anything starts

The override relies on Compose's !override and !reset tags, so the script checks the Docker Compose version first and stops if it is too old.

Lean by default

A small server has to be shared, so every project starts lean. Storage and image processing are off — files go to object storage instead — and Studio only runs when we need it. Optional services are enabled with Compose profiles, not by editing files. Projects we are not working on can be stopped without losing data.

Nothing listens on the public internet

Every container port binds to 127.0.0.1 only, and the script refuses a config where any port does not. Public traffic arrives through a Cloudflare Tunnel: cloudflared makes an outbound connection, so the server needs no open web ports and the origin cannot be reached directly.

Because requests arrive through Cloudflare, Auth's rate limiting reads the CF-Connecting-IP header to see the real client address.

Auth defaults we change

Upstream defaults are friendly for local development but too open for production. The script turns off phone sign-up with auto-confirm, since there is no SMS provider. For our control-plane database, sign-ups are disabled completely; admin users are created server-side, and row-level security requires both an admin role in app_metadata and a multi-factor (aal2) session. The public anon role has no access to private schemas at all.

Operations lessons

  • Back up before every migration. An encrypted dump is taken before changes are applied to a database with data.
  • Reload the API schema after migrations. PostgREST caches the schema; we send notify pgrst, 'reload schema' after applying SQL, otherwise new functions return errors until a restart.
  • Test the security policies. A SQL script checks the access rules inside a transaction that is rolled back and prints pass or fail for each check.

The honest trade-off

One free server is a single point of failure. That is acceptable for development, launches and early traction, and it keeps running costs at zero. When a product earns revenue, the next step is a second server with a replica — the same scripts make that move straightforward.