# Foglamp self-host stack — from zero to instrumented with one command: # # docker compose up --build # # Then open the dashboard at http://localhost:3001 and log in with the admin # credentials printed once by the `migrate` service (search its logs for # "Save these now"). Point the SDK at the ingest API on http://localhost:4000. # # Plain Postgres + ClickHouse + Redis — no queue. Redis only backs # ingest rate limiting; remove the service (and REDIS_URL) and ingest falls back # to per-instance in-memory limiting. Email is optional: leave RESEND_API_KEY # unset and you still log in via the seeded email+password admin. # # Override secrets/URLs with a local `.env` next to this file, e.g. # BETTER_AUTH_SECRET=... (REQUIRED in production; the default is dev-only) # RESEND_API_KEY=... (enables magic-link + alert emails) # ADMIN_EMAIL=... ADMIN_PASSWORD=... (otherwise a random password is printed) # Shared environment for the backend tiers (server, ingest, migrate). Service # DNS names (postgres, clickhouse, server) resolve on the default compose # network; the published ports are what the host browser / SDK use. x-app-env: &app-env NODE_ENV: production DATABASE_URL: postgres://foglamp:foglamp@postgres:5432/foglamp BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-dev_only_secret_change_me_0123456789abcd} BETTER_AUTH_URL: http://localhost:3000 CORS_ORIGIN: http://localhost:3001 CLICKHOUSE_URL: http://clickhouse:8123 CLICKHOUSE_USER: default CLICKHOUSE_PASSWORD: "" CLICKHOUSE_DATABASE: foglamp OPENROUTER_MODELS_URL: https://openrouter.ai/api/v1/models RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} AUTH_DISABLE_EMAIL_PASSWORD: ${AUTH_DISABLE_EMAIL_PASSWORD:-} ADMIN_EMAIL: ${ADMIN_EMAIL:-} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} REDIS_URL: redis://redis:6379 services: postgres: image: postgres:17-alpine environment: POSTGRES_USER: foglamp POSTGRES_PASSWORD: foglamp POSTGRES_DB: foglamp volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U foglamp -d foglamp"] interval: 5s timeout: 5s retries: 10 clickhouse: image: clickhouse/clickhouse-server:24.8-alpine environment: CLICKHOUSE_DB: foglamp # Without a USER/PASSWORD set on the server, the image locks the `default` # user to localhost only — which breaks the app tiers connecting over the # compose network (AUTHENTICATION_FAILED). SKIP_USER_SETUP keeps `default` # passwordless WITH network access, matching the empty CLICKHOUSE_PASSWORD # the apps use. ClickHouse is not published to the host, so it's reachable # only inside this network. To harden, set CLICKHOUSE_USER/PASSWORD here and # in x-app-env instead, and drop this line. CLICKHOUSE_SKIP_USER_SETUP: "1" volumes: - ch_data:/var/lib/clickhouse ulimits: nofile: soft: 262144 hard: 262144 healthcheck: test: ["CMD", "clickhouse-client", "--query", "SELECT 1"] interval: 5s timeout: 5s retries: 15 redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 10 # One-shot: Postgres migrate + ClickHouse DDL + retention + seed, then exits. # The app tiers wait for it to complete successfully. migrate: build: context: . target: migrate environment: *app-env depends_on: postgres: condition: service_healthy clickhouse: condition: service_healthy restart: "no" server: build: context: . target: server environment: <<: *app-env PORT: "3000" ports: - "3000:3000" depends_on: postgres: condition: service_healthy clickhouse: condition: service_healthy migrate: condition: service_completed_successfully ingest: build: context: . target: ingest environment: <<: *app-env INGEST_PORT: "4000" ports: - "4000:4000" depends_on: postgres: condition: service_healthy clickhouse: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully web: build: context: . target: web args: NEXT_PUBLIC_SERVER_URL: http://localhost:3000 NEXT_PUBLIC_APP_URL: http://localhost:3001 environment: NODE_ENV: production PORT: "3001" # Baked into the client bundle (browser-facing); kept here for parity. NEXT_PUBLIC_SERVER_URL: http://localhost:3000 NEXT_PUBLIC_APP_URL: http://localhost:3001 # SSR session gate reaches the server over the internal network. INTERNAL_SERVER_URL: http://server:3000 ports: - "3001:3001" depends_on: server: condition: service_started migrate: condition: service_completed_successfully volumes: pg_data: ch_data: redis_data: