self-hosting
Docker Compose

Docker Compose

The fastest path from zero to a running Orsa instance. This guide walks through every step — clone, configure, start, verify.

Quick Start (5 minutes)

# 1. Clone the repo
git clone https://github.com/paragonhq/orsa.git
cd orsa
 
# 2. Copy environment file
cp .env.example .env.local
 
# 3. Start Supabase locally
supabase start
 
# 4. Start Redis + Browser Worker
docker compose up -d
 
# 5. Run database migrations
supabase db push
 
# 6. Seed the database (optional — loads NAICS codes, test data)
pnpm db:seed
 
# 7. Install dependencies and start dev servers
pnpm install
pnpm dev

Your services are now running:

docker-compose.yml Walkthrough

The provided docker-compose.yml runs the infrastructure services that Orsa depends on. The application itself (API, Web) runs via pnpm dev for development or deploys to Vercel/your host for production.

version: '3.8'
 
services:
  # ─── Redis ────────────────────────────────────────────────────
  # Cache layer + rate limiting backend.
  # In production, replace with Upstash Redis or a managed Redis instance.
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 5s
      retries: 3
 
  # ─── Browser Worker ───────────────────────────────────────────
  # Playwright Chromium pool for scraping, screenshots, JS rendering.
  # In production, deploy to Fly.io or a dedicated machine.
  browser-worker:
    build:
      context: ./services/browser-worker
      dockerfile: Dockerfile
    ports:
      - '3002:3002'
    environment:
      - PORT=3002
      - NODE_ENV=development
      - BROWSER_POOL_SIZE=3        # Concurrent browser instances
      - MAX_CONCURRENT_PAGES=10    # Total open pages across all browsers
      - PAGE_TIMEOUT=30000         # 30s per-page timeout
    depends_on:
      redis:
        condition: service_healthy
    volumes:
      - ./services/browser-worker/src:/app/src  # Hot reload in dev
    # Required for Chromium:
    shm_size: '512mb'
 
volumes:
  redis-data:

Key Configuration

VariableDefaultDescription
BROWSER_POOL_SIZE3Number of concurrent Chromium instances. Each uses ~200-400 MB RAM.
MAX_CONCURRENT_PAGES10Maximum open pages across all browser instances.
PAGE_TIMEOUT30000Milliseconds before a page load is considered failed.
shm_size512mbShared memory for Chromium. Must be ≥256 MB.

Production Docker Compose

For production self-hosting without Kubernetes, use this extended compose file:

version: '3.8'
 
services:
  # ─── API ──────────────────────────────────────────────────────
  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    ports:
      - '3001:3001'
    env_file:
      - .env.local
    environment:
      - NODE_ENV=production
      - PORT=3001
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
 
  # ─── Web Dashboard ───────────────────────────────────────────
  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile
    ports:
      - '3000:3000'
    env_file:
      - .env.local
    environment:
      - NODE_ENV=production
      - PORT=3000
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
 
  # ─── Redis ────────────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 768M
          cpus: '0.5'
 
  # ─── Browser Worker ───────────────────────────────────────────
  browser-worker:
    build:
      context: ./services/browser-worker
      dockerfile: Dockerfile
    ports:
      - '3002:3002'
    environment:
      - PORT=3002
      - NODE_ENV=production
      - BROWSER_POOL_SIZE=5
      - MAX_CONCURRENT_PAGES=20
      - PAGE_TIMEOUT=30000
    depends_on:
      redis:
        condition: service_healthy
    shm_size: '1gb'
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: '2.0'
 
  # ─── Reverse Proxy ───────────────────────────────────────────
  caddy:
    image: caddy:2-alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - api
      - web
    restart: unless-stopped
 
volumes:
  redis-data:
  caddy-data:
  caddy-config:

Create a Caddyfile in the project root:

api.yourdomain.com {
    reverse_proxy api:3001
}

app.yourdomain.com {
    reverse_proxy web:3000
}

browser.yourdomain.com {
    reverse_proxy browser-worker:3002
}

Step-by-Step Setup

1. Clone and Configure

git clone https://github.com/paragonhq/orsa.git
cd orsa
cp .env.example .env.local

Edit .env.local with your actual values. See the Configuration page for every variable.

2. Start Supabase

For local development, use the Supabase CLI:

# Install Supabase CLI
brew install supabase/tap/supabase  # macOS
# or: npx supabase@latest            # any OS
 
# Start local Supabase stack (Postgres, Auth, Storage, Studio)
supabase start

This outputs connection details. Update .env.local:

NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=<output from supabase start>
SUPABASE_SERVICE_ROLE_KEY=<output from supabase start>

For production, use Supabase Cloud (opens in a new tab) or self-hosted Supabase. See Providers → Supabase.

3. Run Migrations

# Apply all SQL migrations to the database
supabase db push

This runs all files in supabase/migrations/ in order:

  1. 00001_initial_schema.sql — Core tables (brands, users, API keys, credits, crawl jobs, usage events, audit log)
  2. 00002_rls_policies.sql — Row Level Security policies
  3. 00003_seed_data.sql — NAICS codes and reference data
  4. 00004_merchant_descriptors_expansion.sql — Merchant identification tables
  5. 00005_webhooks.sql — Webhook delivery tables

4. Start Infrastructure Services

docker compose up -d

Verify everything is running:

# Check containers
docker compose ps
 
# Test Redis
docker compose exec redis redis-cli ping
# → PONG
 
# Test Browser Worker
curl http://localhost:3002/health
# → {"status":"ok","browsers":3,"pages":0}

5. Install Dependencies and Start

pnpm install
pnpm dev

This starts all workspace packages in development mode via Turborepo:

  • apps/api on port 3001
  • apps/web on port 3000

6. Seed the Database (Optional)

pnpm db:seed

This runs scripts/seed.ts which loads:

  • NAICS classification codes
  • Test brands for development
  • Sample API keys

7. Verify the Installation

# Health check
curl http://localhost:3001/api/v1/health
 
# Test scraping (no auth required for health, auth required for data endpoints)
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3001/api/v1/web/scrape/markdown?url=https://example.com"
 
# Test brand retrieval
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3001/api/v1/brand/retrieve?domain=stripe.com"

Generating TypeScript Types

After any migration, regenerate the database types:

pnpm generate:types

This runs supabase gen types typescript --local > packages/db/src/types/database.ts and keeps your TypeScript types in sync with the database schema.

Stopping Services

# Stop all containers
docker compose down
 
# Stop and remove volumes (deletes Redis data)
docker compose down -v
 
# Stop Supabase
supabase stop

Next Steps