Supabase Local Development Guide
Status: Complete
Overview
This guide provides step-by-step instructions for setting up Supabase locally using Docker for development and testing purposes. Local development offers complete control, offline capability, and zero costs.
Prerequisites
Required Software
- Docker Desktop: Version 20.10+
- Docker Compose: Version 2.0+ (included with Docker Desktop)
- Node.js: Version 18+ (for client applications)
- Git: For version control
System Requirements
- RAM: Minimum 4GB, recommended 8GB
- Disk Space: 2GB for Docker images
- Ports: Ensure the following ports are available:
- 54321 (Kong API Gateway)
- 54322 (PostgreSQL)
- 54323 (Supabase Studio)
- 54324 (Inbucket email testing)
- 54325 (PostgREST)
- 54326 (GoTrue Auth)
Installation Steps
Step 1: Create Project Directory
# Create project directory
mkdir -p ~/supabase-local
cd ~/supabase-local
# Create subdirectories
mkdir -p config migrations seed data
Step 2: Download Docker Compose Configuration
Create docker-compose.yml:
# Supabase Local Development Stack
version: '3.8'
services:
# PostgreSQL Database
db:
container_name: supabase-db
image: supabase/postgres:15.1.0.147
healthcheck:
test: pg_isready -U postgres -h localhost
interval: 5s
timeout: 5s
retries: 10
ports:
- "54322:5432"
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- ./data/db:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
restart: unless-stopped
# Kong API Gateway
kong:
container_name: supabase-kong
image: kong:2.8.1
ports:
- "54321:8000"
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
volumes:
- ./config/kong.yml:/home/kong/kong.yml:ro
depends_on:
- auth
- rest
- storage
restart: unless-stopped
# GoTrue Authentication
auth:
container_name: supabase-auth
image: supabase/gotrue:v2.99.0
ports:
- "54326:9999"
depends_on:
db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
GOTRUE_DB_DRIVER: postgres
DATABASE_URL: postgresql://postgres:postgres@db:5432/postgres?search_path=auth
GOTRUE_SITE_URL: http://localhost:3000
GOTRUE_URI_ALLOW_LIST: http://localhost:3000
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: 3600
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "true"
GOTRUE_SMTP_HOST: inbucket
GOTRUE_SMTP_PORT: 2500
GOTRUE_SMTP_ADMIN_EMAIL: admin@example.com
GOTRUE_SMTP_MAX_FREQUENCY: 1s
GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
restart: unless-stopped
# PostgREST REST API
rest:
container_name: supabase-rest
image: postgrest/postgrest:v11.2.0
ports:
- "54325:3000"
depends_on:
db:
condition: service_healthy
environment:
PGRST_DB_URI: postgresql://postgres:postgres@db:5432/postgres
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
restart: unless-stopped
# Realtime WebSockets
realtime:
container_name: supabase-realtime
image: supabase/realtime:v2.25.35
ports:
- "54327:4000"
depends_on:
db:
condition: service_healthy
environment:
DB_HOST: db
DB_PORT: 5432
DB_NAME: postgres
DB_USER: postgres
DB_PASSWORD: postgres
DB_SSL: "false"
PORT: 4000
JWT_SECRET: ${JWT_SECRET}
REPLICATION_MODE: RLS
REPLICATION_POLL_INTERVAL: 100
SECURE_CHANNELS: "true"
SLOT_NAME: supabase_realtime_rls
TEMPORARY_SLOT: "true"
restart: unless-stopped
# Storage API
storage:
container_name: supabase-storage
image: supabase/storage-api:v0.43.11
ports:
- "54325:5000"
depends_on:
db:
condition: service_healthy
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgresql://postgres:postgres@db:5432/postgres
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
volumes:
- ./data/storage:/var/lib/storage
restart: unless-stopped
# Supabase Studio
studio:
container_name: supabase-studio
image: supabase/studio:20231123-ce42139
ports:
- "54323:3000"
environment:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: postgres
DEFAULT_ORGANIZATION_NAME: Default Organization
DEFAULT_PROJECT_NAME: Default Project
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: http://localhost:54321
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
depends_on:
- meta
restart: unless-stopped
# PostgreSQL Meta Service
meta:
container_name: supabase-meta
image: supabase/postgres-meta:v0.68.0
ports:
- "54328:8080"
depends_on:
db:
condition: service_healthy
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: db
PG_META_DB_PORT: 5432
PG_META_DB_NAME: postgres
PG_META_DB_USER: postgres
PG_META_DB_PASSWORD: postgres
restart: unless-stopped
# Email Testing (Inbucket)
inbucket:
container_name: supabase-inbucket
image: inbucket/inbucket:3.0.3
ports:
- "54324:9000" # Web UI
- "2500:2500" # SMTP
- "1100:1100" # POP3
restart: unless-stopped
volumes:
db-data:
storage-data:
networks:
default:
name: supabase-network
Step 3: Create Environment Configuration
Create .env file:
# JWT Secrets (Generate with: openssl rand -hex 32)
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
# API Configuration
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres
Step 4: Generate Secure Keys
# Generate JWT Secret
echo "JWT_SECRET=$(openssl rand -hex 32)"
# For production, also generate new ANON and SERVICE keys
# Use the JWT generator at https://supabase.com/docs/guides/self-hosting#api-keys
Step 5: Create Kong Configuration
Create config/kong.yml:
_format_version: "2.1"
_transform: true
services:
- name: auth-v1
url: http://auth:9999/
routes:
- name: auth-v1-all
strip_path: true
paths:
- /auth/v1/
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: false
paths:
- /rest/v1/
- name: realtime-v1
url: http://realtime:4000/socket/
routes:
- name: realtime-v1-all
strip_path: true
paths:
- /realtime/v1/
- name: storage-v1
url: http://storage:5000/
routes:
- name: storage-v1-all
strip_path: true
paths:
- /storage/v1/
- name: meta
url: http://meta:8080/
routes:
- name: meta-all
strip_path: true
paths:
- /pg/
consumers:
- username: anon
keyauth_credentials:
- key: ${ANON_KEY}
- username: service_role
keyauth_credentials:
- key: ${SERVICE_ROLE_KEY}
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- anon
- service_role
Step 6: Initialize Database Schema
Create migrations/00_initial_schema.sql:
-- Enable extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Create auth schema
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS storage;
-- Create basic user profiles table
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID REFERENCES auth.users ON DELETE CASCADE PRIMARY KEY,
email TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Create policy: Users can view their own profile
CREATE POLICY "Users can view own profile" ON public.profiles
FOR SELECT USING (auth.uid() = id);
-- Create policy: Users can update their own profile
CREATE POLICY "Users can update own profile" ON public.profiles
FOR UPDATE USING (auth.uid() = id);
-- Create trigger to create profile on user signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $
BEGIN
INSERT INTO public.profiles (id, email)
VALUES (new.id, new.email);
RETURN new;
END;
$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
Step 7: Start Services
# Start all services
docker-compose up -d
# Check service status
docker-compose ps
# View logs
docker-compose logs -f
# View specific service logs
docker-compose logs -f db
docker-compose logs -f auth
Verification
Service Health Checks
# Check database
docker exec supabase-db psql -U postgres -c "SELECT version();"
# Check Kong API Gateway
curl http://localhost:54321/auth/v1/health
# Check PostgREST
curl http://localhost:54325/
# Check Studio is accessible
open http://localhost:54323
Test Authentication
# Test signup (will auto-confirm with current settings)
curl -X POST 'http://localhost:54321/auth/v1/signup' \
-H "apikey: ${ANON_KEY}" \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "testpassword123"
}'
Development Workflow
1. Database Migrations
# Create new migration
touch migrations/$(date +%Y%m%d%H%M%S)_migration_name.sql
# Apply migration
docker exec -i supabase-db psql -U postgres < migrations/your_migration.sql
# Export schema
docker exec supabase-db pg_dump -U postgres --schema-only > schema.sql
2. Seed Data
# Create seed file
cat > seed/seed_data.sql << EOF
-- Insert test data
INSERT INTO profiles (id, email, full_name) VALUES
('00000000-0000-0000-0000-000000000001', 'user1@example.com', 'Test User 1'),
('00000000-0000-0000-0000-000000000002', 'user2@example.com', 'Test User 2');
EOF
# Apply seed data
docker exec -i supabase-db psql -U postgres < seed/seed_data.sql
3. Client Connection
// Install client
// npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'http://localhost:54321',
'your-anon-key'
)
// Test connection
const { data, error } = await supabase
.from('profiles')
.select('*')
console.log({ data, error })
Common Tasks
Reset Database
# Stop services
docker-compose down
# Remove data volumes
docker volume rm supabase-local_db-data
# Restart
docker-compose up -d
Backup Database
# Create backup
docker exec supabase-db pg_dump -U postgres > backup_$(date +%Y%m%d_%H%M%S).sql
# Restore backup
docker exec -i supabase-db psql -U postgres < backup.sql
Update Supabase Images
# Pull latest images
docker-compose pull
# Restart with new images
docker-compose up -d
Troubleshooting
Port Conflicts
# Check what's using a port
lsof -i :54321
# Change ports in docker-compose.yml if needed
Database Connection Issues
# Check database logs
docker logs supabase-db
# Test connection
docker exec supabase-db pg_isready -U postgres
# Reset database password
docker exec supabase-db psql -U postgres -c "ALTER USER postgres PASSWORD 'postgres';"
Storage Issues
# Check storage permissions
docker exec supabase-storage ls -la /var/lib/storage
# Fix permissions
docker exec supabase-storage chown -R 1000:1000 /var/lib/storage
Memory Issues
# Increase Docker memory allocation
# Docker Desktop > Preferences > Resources > Memory
# Check container resource usage
docker stats
Best Practices
- Version Control: Keep docker-compose.yml and migrations in Git
- Environment Variables: Never commit .env files with real secrets
- Data Persistence: Regular backups of data volumes
- Testing: Use separate databases for testing
- Monitoring: Watch logs during development
- Updates: Regularly update Docker images
- Documentation: Document custom configurations
Next Steps
- Set up your application to connect to local Supabase
- Configure authentication providers
- Design your database schema
- Implement Row Level Security policies
- Set up real-time subscriptions
- Configure storage buckets