DEV Community

Chris F A
Chris F A

Posted on

How I Ran the Full Supabase Stack Locally in 100 MB of RAM (No Docker)

How I Ran the Full Supabase Stack Locally in 100 MB of RAM (No Docker)

Twelve containers. 1.6 GB of RAM. 45 seconds to supabase start. Every time I opened my project.

This week, the same stack runs in ~100 MB, boots in 2.5 seconds, and my app code didn't change. Here's how.

Why this matters

Docker overhead for local dev is a background tax that's easy to ignore. My M1 Air fans spin up whenever Supabase is running. My battery drains ~30% faster. My other tools (VS Code, browsers, IDE plugins) share the remaining RAM with 12 containers I don't think about.

The real cost isn't the RAM itself, it's the workflows that get shaped around it:

  • I don't run local backup unless I know I'll code for >30 min (not worth the boot).
  • When I'm designing a schema change, I test it against hosted Supabase because local is slow.
  • Anyone on my team who isn't a full-time backend engineer (designers, PMs) uses staging instead of local, which creates its own dysfunction.

The alternative: a single-binary Supabase-compatible backend

The tool I ended up using is Tinbase — a single-file executable (~58 MB) that runs real Postgres 17 plus auth, realtime, edge functions, webhooks, and cron. It exposes the same HTTP + Postgres endpoints as hosted Supabase, so supabase-js connects to it unchanged.

From my app's perspective, the only change was the connection string:

ts
// Before (Docker Supabase local)
const supabase = createClient(
'http://localhost:54321',
'eyJhbGci...anon-key'
);

// After (Tinbase)
const supabase = createClient(
'http://localhost:4000',
'eyJhbGci...anon-key' // same SDK, same anon key format
);

That's it for app code. Zero refactor.

The migration steps

1. Install

bash
curl -fsSL https://tinbase.dev/install.sh | sh

One executable at ~/.tinbase/bin/tinbase. ~58 MB on disk.

2. Initialize a project

bash
tinbase init my-project
cd my-project
tinbase start

Boots in ~2.5s. Exposes:

  • Postgres on localhost:5432
  • REST API on localhost:4000/rest/v1
  • Auth on localhost:4000/auth/v1
  • Realtime WebSocket on localhost:4000/realtime/v1
  • Storage on localhost:4000/storage/v1
  • Admin UI on localhost:4000

Same endpoint structure as hosted Supabase.

3. Port migrations

bash
cp -r /path/to/my-app/supabase/migrations ./migrations
tinbase db reset

The migrations run against the embedded Postgres. RLS policies work identically because it's real Postgres 17.

4. Update the connection string in my app

[7:04 AM]Change the SUPABASE_URL env var from http://localhost:54321 to http://localhost:4000. That's the only app-side change.

5. Stop Docker

bash
supabase stop
docker system prune -a --volumes # reclaim ~2.3 GB of images + volumes

Benchmarks after the switch

Measured on the same M1 Air, same app, same test load (10 min of active development: a few queries, a realtime subscription, one edge function invocation).

Metric Docker Supabase Tinbase Delta
Boot time (cold) 45s 2.5s −94%
Boot time (warm) 15s 1.2s −92%
Steady-state RAM 1626 MB 98 MB −94%
Disk footprint 2.3 GB 58 MB −97%
Idle CPU % 4-8% 0.1-0.4% −95%
Fan noise audible silent qualitative

The RAM number is the headline. Under active load with realtime + edge functions firing, RAM peaked at ~250 MB. Docker Supabase under the same load peaked north of 2 GB.

What worked out of the box

  • supabase-js SDK: unchanged. All CRUD, auth, realtime, storage calls work identically.
  • RLS policies: identical behavior (it IS real Postgres 17, same policy engine).
  • Edge functions: deployed the same way (tinbase functions deploy hello), runtime is Deno like Supabase.
  • Realtime subscriptions: WebSocket connection, same protocol.
  • Storage: same bucket/RLS semantics.
  • Migrations: standard Postgres SQL, ran without modification.

What I had to change

Being honest — a few things weren't 100% drop-in:

1. Supabase Studio → Tinbase Admin UI. The admin UI is different (obviously — it's a different product). If my team relied on Studio-specific features, I'd have to check compat. In my case, I mostly use it for eyeballing schema, which the Tinbase UI does fine.

2. Some newer Supabase-hosted features aren't there yet. Specifically, some of the newer analytics endpoints and the vector search integration are hosted-only for now. If my app depended on these, I'd need to keep them talking to hosted Supabase (Tinbase for the general dev workflow, hosted Supabase for those specific features).

3. Edge function environment isn't 100% identical. Deno version, some environment variables, some Supabase-specific runtime globals. I hit one edge case where I was using Deno.env.get('SUPABASE_URL') — worked in the local runtime, but the value was different. Easy fix, but worth mentioning.

Where the single-binary approach hits limits

This isn't a Supabase replacement. It's a local-dev replacement.

For production, hosted Supabase is still the right choice — you want their SLA, their scaling infrastructure, their team maintaining it. What Tinbase replaces is the local-dev docker-compose stack.

The approach starts to strain if:

  • You have multi-terabyte data volumes locally: fine for dev-sized datasets, less optimized for large ones (though it's real Postgres, so it scales as Postgres does).
  • You need every hosted-Supabase feature identically: check the compat matrix.
  • You're running this in prod at scale: it can, but that's not the primary use case.

For a workflow where I want the Supabase ergonomics without the Docker overhead — which is 95% of my local dev — it's genuinely better.

Takeaway

If you're on Docker Supabase for local dev and the RAM/boot-time overhead is a background tax, there's a lighter alternative now. Same SDK, same auth/realtime/storage semantics, one file. Migration is a connection string swap.

The Docker overhead was invisible to me until I removed it. Now the difference is stark: fans off, faster iteration, and (unexpectedly) designers on my team started running the app locally because the setup was one command.

Sometimes the right optimization isn't a smaller container image. It's asking whether you need the container abstraction at all.

Top comments (0)