# Production docker-compose for Spanlens self-hosted deployment. # # Prerequisites: # 1. Apply the database schema (one-time setup): # Open Supabase Dashboard → SQL Editor → New query # Paste the contents of supabase/init.sql and click Run # 2. Create a .env file in this directory with the required variables below. # 3. Run: docker compose up -d # # Required .env variables: # NEXT_PUBLIC_SUPABASE_URL e.g. https://abc.supabase.co # NEXT_PUBLIC_SUPABASE_ANON_KEY eyJ... # SUPABASE_URL same as NEXT_PUBLIC_SUPABASE_URL (used by server) # SUPABASE_ANON_KEY same as NEXT_PUBLIC_SUPABASE_ANON_KEY # SUPABASE_SERVICE_ROLE_KEY service_role key (never expose to browser) # ENCRYPTION_KEY 32-byte base64 string (see apps/server/.env.example) # CRON_SECRET random secret for cron endpoint auth # # Optional .env variables: # WEB_URL public URL of the web app (for invite emails) # RESEND_API_KEY for transactional email # RESEND_FROM sender address (e.g. Spanlens ) # PADDLE_API_KEY / PADDLE_NOTIFICATION_SECRET / PADDLE_ENVIRONMENT # PADDLE_PRICE_STARTER / PADDLE_PRICE_TEAM / PADDLE_PRICE_ENTERPRISE # # Postgres pooled connection (the `requests` log is read and written over this): # SUPABASE_DB_POOLER_URL transaction-mode pooler URL on port 6543 # (Supabase Dashboard -> Project Settings -> # Database -> Connection string -> Transaction pooler). # A full DB credential: never log it. services: web: # Default: pull pre-built image from GHCR (no build required). # docker-entrypoint.sh patches NEXT_PUBLIC_* placeholders at startup. # To build locally instead: docker compose up -d --build image: ghcr.io/spanlens/spanlens-web:latest build: context: . dockerfile: apps/web/Dockerfile # Build args override the placeholder defaults so the bundle already # contains real values (entrypoint sed is then a no-op). Omit args # to rely on entrypoint patching instead (works either way). args: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} API_URL: http://server:3001 ports: - '3000:3000' environment: API_URL: http://server:3001 NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} depends_on: server: condition: service_healthy restart: unless-stopped healthcheck: test: ['CMD-SHELL', 'wget --quiet --spider http://localhost:3000/ || exit 1'] interval: 30s timeout: 10s start_period: 20s retries: 3 server: # Default: pull pre-built image from GHCR. # To build locally instead: docker compose up -d --build image: ghcr.io/spanlens/spanlens-server:latest build: context: . dockerfile: apps/server/Dockerfile ports: - '3001:3001' environment: PORT: 3001 SUPABASE_URL: ${SUPABASE_URL} SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} ENCRYPTION_KEY: ${ENCRYPTION_KEY} CRON_SECRET: ${CRON_SECRET} WEB_URL: ${WEB_URL:-http://localhost:3000} RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_FROM: ${RESEND_FROM:-} PADDLE_API_KEY: ${PADDLE_API_KEY:-} PADDLE_NOTIFICATION_SECRET: ${PADDLE_NOTIFICATION_SECRET:-} PADDLE_ENVIRONMENT: ${PADDLE_ENVIRONMENT:-sandbox} PADDLE_PRICE_STARTER: ${PADDLE_PRICE_STARTER:-} PADDLE_PRICE_TEAM: ${PADDLE_PRICE_TEAM:-} PADDLE_PRICE_ENTERPRISE: ${PADDLE_PRICE_ENTERPRISE:-} SUPABASE_DB_POOLER_URL: ${SUPABASE_DB_POOLER_URL} restart: unless-stopped healthcheck: test: ['CMD-SHELL', 'wget --quiet --spider http://localhost:3001/health || exit 1'] interval: 30s timeout: 5s start_period: 10s retries: 3