Last updated: Aug 4, 2025, 11:26 AM UTC

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

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

  1. Version Control: Keep docker-compose.yml and migrations in Git
  2. Environment Variables: Never commit .env files with real secrets
  3. Data Persistence: Regular backups of data volumes
  4. Testing: Use separate databases for testing
  5. Monitoring: Watch logs during development
  6. Updates: Regularly update Docker images
  7. 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

Resources