A lightweight, self-contained Supabase-compatible backend in a single Docker container. Perfect for local development, testing, and learning.
Micro-Supabase packages the core Supabase services into one Docker image (~337MB):
- pglited - PostgreSQL-compatible in-memory database
- GoTrue v2.186.0 - Authentication service with JWT support
- PostgREST v14.4 - Auto-generated REST API from your database schema
- Nginx - Reverse proxy providing a single Supabase-js compatible endpoint
Note: Currently built for ARM64 architecture (Apple Silicon, AWS Graviton).
- ✅ Full Supabase authentication (signup, signin, JWT tokens)
- ✅ Auto-generated REST API from database schema
- ✅ Row Level Security (RLS) policies
- ✅ Automatic migration execution on startup
- ✅ Supabase-js client compatible
- ✅ Configurable ports
- ✅ Single command to run
./build.shIn-memory mode (default, data lost on restart):
docker run -d --name micro-supabase \
-p 54321:54321 \
-p 8080:8080 \
-v "$(pwd)/migrations:/app/migrations" \
micro-supabase:latestPersistent mode (data retained across restarts):
docker run -d --name micro-supabase \
-p 54321:54321 \
-p 8080:8080 \
-e STORAGE_MODE=disk \
-v "$(pwd)/migrations:/app/migrations" \
-v "$(pwd)/data:/app/data" \
micro-supabase:latestNote: Mount your migrations directory with -v "$(pwd)/migrations:/app/migrations" to run migrations on startup.
Check health:
curl http://0.0.0.0:8080/auth/v1/healthSign up a user:
curl -X POST http://0.0.0.0:8080/auth/v1/signup \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-d '{"email":"user@example.com","password":"password123"}'Sign in:
curl -X POST "http://0.0.0.0:8080/auth/v1/token?grant_type=password" \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-d '{"email":"user@example.com","password":"password123"}'Configure ports and storage mode by passing environment variables:
docker run -d --name micro-supabase \
-p 55432:54321 \
-p 9090:8080 \
-e STORAGE_MODE=disk \
-e PG_PORT=54321 \
-e AUTH_PORT=9999 \
-e POSTGREST_PORT=3000 \
-e PROXY_PORT=8080 \
-v "$(pwd)/migrations:/app/migrations" \
-v "$(pwd)/data:/app/data" \
micro-supabase:latestAvailable variables:
STORAGE_MODE- Storage mode:memory(default, ephemeral) ordisk(persistent)PG_PORT- PostgreSQL port (default: 54321)AUTH_PORT- Auth service port (default: 9999)POSTGREST_PORT- PostgREST port (default: 3000)PROXY_PORT- Nginx proxy port (default: 8080)
Storage Modes:
memory(default): Data stored in memory, lost when container stops. Fast, ideal for testing.disk: Data persisted to/app/data/pglitedirectory. Requires volume mount:-v "$(pwd)/data:/app/data"
Only two ports are exposed to the host:
54321- PostgreSQL direct access8080- Single Supabase-js endpoint (Auth + REST API)
Place SQL migration files in a directory and mount it to /app/migrations:
docker run -d --name micro-supabase \
-p 54321:54321 \
-p 8080:8080 \
-v "$(pwd)/migrations:/app/migrations" \
micro-supabase:latestMigration example (001_create_posts.sql):
-- Create posts table
CREATE TABLE public.posts (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
-- RLS policies using auth.uid()
CREATE POLICY "Users can view their own posts" ON public.posts
FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create their own posts" ON public.posts
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Grant permissions
GRANT ALL ON TABLE public.posts TO authenticated;
GRANT SELECT ON TABLE public.posts TO anon;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;Migrations run automatically on container startup in alphabetical order.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'http://0.0.0.0:8080',
'super-secret-jwt-token-with-at-least-32-characters-long'
)
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123'
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123'
})
// Query data (with automatic RLS)
const { data: posts } = await supabase
.from('posts')
.select('*')Signup:
curl -X POST http://0.0.0.0:8080/auth/v1/signup \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-d '{"email":"user@example.com","password":"password123"}'Signin:
curl -X POST "http://0.0.0.0:8080/auth/v1/token?grant_type=password" \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-d '{"email":"user@example.com","password":"password123"}'Get user:
curl http://0.0.0.0:8080/auth/v1/user \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"List all items:
curl http://0.0.0.0:8080/rest/v1/todos \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"Create item:
curl -X POST http://0.0.0.0:8080/rest/v1/todos \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Prefer: return=representation" \
-d '{"user_id":"USER_ID","title":"My todo","completed":false}'Update item:
curl -X PATCH "http://0.0.0.0:8080/rest/v1/todos?id=eq.1" \
-H "Content-Type: application/json" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Prefer: return=representation" \
-d '{"completed":true}'Delete item:
curl -X DELETE "http://0.0.0.0:8080/rest/v1/todos?id=eq.1" \
-H "apikey: super-secret-jwt-token-with-at-least-32-characters-long" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"A demo React + TypeScript app is included in the demo/ folder.
cd demo
npm install
npm run devThe demo shows:
- Sign up / Sign in / Sign out
- Todo CRUD operations
- Row Level Security in action
- Session persistence
Connect directly to the database:
# Using psql from host
psql "postgresql://postgres:password@127.0.0.1:54321/template1"
# Using Docker exec
docker exec micro-supabase psql \
"postgresql://postgres:password@127.0.0.1:54321/template1?sslmode=disable" \
-c "SELECT email FROM auth.users;"View logs:
docker logs micro-supabaseFollow logs:
docker logs -f micro-supabaseCheck running services:
docker exec micro-supabase ps auxList users:
docker exec micro-supabase psql \
"postgresql://postgres:password@127.0.0.1:54321/template1?sslmode=disable" \
-c "SELECT id, email, created_at FROM auth.users;"Stop and remove:
docker rm -f micro-supabaseRebuild:
./build.sh
docker rm -f micro-supabase
# In-memory mode
docker run -d --name micro-supabase \
-p 54321:54321 \
-p 8080:8080 \
-v "$(pwd)/migrations:/app/migrations" \
micro-supabase:latest
# Or with persistent storage
docker run -d --name micro-supabase \
-p 54321:54321 \
-p 8080:8080 \
-e STORAGE_MODE=disk \
-v "$(pwd)/migrations:/app/migrations" \
-v "$(pwd)/data:/app/data" \
micro-supabase:latestMicro-Supabase starts up quickly thanks to the embedded pgdata_seed.tar:
| Storage Mode | Average Startup Time |
|---|---|
| Memory (default) | ~5.9 seconds |
| Disk (persistent) | ~5.8 seconds |
Benchmark details:
- Measured from container start to "Micro-Supabase is running!" message
- Includes: PostgreSQL initialization, Auth service startup, PostgREST startup, Nginx configuration
- Tested on Apple Silicon (ARM64)
- Disk mode is slightly faster due to pre-initialized database state
The complete Docker image is 365MB, containing:
- PostgreSQL runtime (pglited with embedded pgdata): 94.5 MB
- PostgREST: 71.4 MB
- GoTrue (Auth): 27.8 MB
- Debian base + dependencies: 171.6 MB
flowchart TB
subgraph Docker["Docker Container (micro-supabase)"]
direction TB
subgraph Services["Internal Services"]
PG[("pglited<br/>PostgreSQL<br/>:54321")]
Auth["GoTrue<br/>Auth Service<br/>:9999"]
REST["PostgREST<br/>REST API<br/>:3000"]
Proxy["Nginx<br/>Reverse Proxy<br/>:8080"]
end
Proxy -->|"/auth/v1/*"| Auth
Proxy -->|"/rest/v1/*"| REST
Auth -->|"SQL queries"| PG
REST -->|"SQL queries"| PG
end
Client([Client<br/>Supabase-js]) -->|"HTTP :8080<br/>(public)"| Proxy
Client -.->|"PostgreSQL :54321<br/>(public)"| PG
Service Flow:
- Client connects to Nginx on port 8080
- Nginx routes
/auth/v1/*to GoTrue (port 9999) - Nginx routes
/rest/v1/*to PostgREST (port 3000) - Both services connect to pglited on port 54321
- Uses PostgreSQL's
template1database - Creates two schemas:
auth(for GoTrue) andpublic(for your data) - Sets
search_path=auth,publicso services can find tables - Pre-creates auth types needed by GoTrue migrations
- User signs up via
/auth/v1/signup - GoTrue creates user in
auth.userstable - GoTrue returns JWT token with user ID in claims
- Client includes JWT in
Authorization: Bearer TOKENheader - PostgREST validates JWT and sets
request.jwt.claim.sub(user ID) - RLS policies use
auth.uid()to check ownership
RLS policies automatically filter data based on the authenticated user:
-- Only show user's own records
CREATE POLICY "policy_name" ON table_name
FOR SELECT
USING (auth.uid() = user_id);The auth.uid() function extracts the user ID from the JWT token.
- No realtime subscriptions - Realtime subscriptions are not implemented yet
- No storage API - File upload/download not implemented
- Auto-confirm emails - Email verification is skipped for local development
- Single database - Uses
template1, not configurable due to pglited limitations - ARM64 only - Docker image is built for ARM64 architecture (Apple Silicon, AWS Graviton). x86_64 support requires rebuilding binaries
- JWT secret is hardcoded:
super-secret-jwt-token-with-at-least-32-characters-long - Database password is
password - Email auto-confirm is enabled
- CORS is wide open (
*)
For production, change these values and configure proper secrets management.
Container won't start:
- Check if ports 54321 or 8080 are already in use
- Use
docker logs micro-supabaseto see errors
Can't connect from host:
- Make sure you're using
0.0.0.0notlocalhostin the URL - Check firewall settings
Migrations failing:
- Ensure migrations are valid SQL
- Check logs with
docker logs micro-supabase - Migrations must use
auth.uid()for RLS, not direct user_id checks
RLS not working:
- Verify JWT token is included in Authorization header
- Check that table has
ENABLE ROW LEVEL SECURITY - Ensure policies use
auth.uid() = user_id
This is an experimental project demonstrating how to package Supabase services in a single container. Contributions and improvements are welcome!
This project packages several open source components:
- pglited (MIT)
- GoTrue (MIT)
- PostgREST (MIT)
- Nginx (BSD)
Each component retains its original license.