From b515d1877f4178a1011a214686333311a379c878 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:33:56 +0300 Subject: [PATCH 1/9] fix(db): provide containerized self-host schema initialization --- .env.example | 4 +-- .github/workflows/docker-publish.yml | 14 ++++---- .github/workflows/health-check.yml | 12 +++++++ README.md | 31 ++++++++++++---- docker-compose.selfhost.yml | 19 +++++++++- init.Dockerfile | 3 +- scripts/test-selfhost-init.sh | 54 ++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 scripts/test-selfhost-init.sh diff --git a/.env.example b/.env.example index 5429a7a34d..6e39da10c3 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ DATABASE_URL="postgres://databuddy:databuddy_dev_password@localhost:5432/databud REDIS_URL="redis://localhost:6379" BULLMQ_REDIS_URL="redis://localhost:6379" -# Required only by docker-compose.selfhost.yml. Use URL-safe passwords and make -# the local database URLs above use the same credentials before initialization. +# Required only by docker-compose.selfhost.yml. Use URL-safe passwords. +# Compose configures the container database URLs from these values. IMAGE_TAG="" POSTGRES_PASSWORD="" CLICKHOUSE_PASSWORD="" diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f4f93f52d4..06a9a5571c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -49,7 +49,7 @@ jobs: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} run: | - ALL='["api","basket","dashboard","insights","links","uptime"]' + ALL='["api","basket","dashboard","init","insights","links","uptime"]' if [[ "$EVENT_NAME" != "push" ]]; then echo "services=$ALL" >> "$GITHUB_OUTPUT" exit 0 @@ -60,19 +60,16 @@ jobs: fi export TURBO_SCM_BASE="$BEFORE_SHA" export TURBO_SCM_HEAD="HEAD" - affected=() + # The init image ships the schema and tooling for every release. + affected=('"init"') for svc in api basket dashboard insights links uptime; do count=$(bunx turbo ls --affected --filter="@databuddy/$svc" --output=json | jq -r '.packages.count') if [[ "$count" != "0" ]]; then affected+=("\"$svc\"") fi done - if [[ ${#affected[@]} -eq 0 ]]; then - echo "services=[]" >> "$GITHUB_OUTPUT" - else - IFS=, - echo "services=[${affected[*]}]" >> "$GITHUB_OUTPUT" - fi + IFS=, + echo "services=[${affected[*]}]" >> "$GITHUB_OUTPUT" build: name: Build ${{ matrix.service }} (${{ matrix.platform.arch }}) @@ -136,6 +133,7 @@ jobs: api) text="Databuddy API service - analytics backend" ;; basket) text="Databuddy Basket service - event ingestion" ;; dashboard) text="Databuddy Dashboard service - web analytics UI" ;; + init) text="Databuddy database initialization and schema tools" ;; insights) text="Databuddy Insights service - queued insight generation" ;; links) text="Databuddy Links service - URL shortening and tracking" ;; uptime) text="Databuddy Uptime service - availability monitoring" ;; diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml index 8deaef1510..e56c5ef4bb 100644 --- a/.github/workflows/health-check.yml +++ b/.github/workflows/health-check.yml @@ -5,6 +5,8 @@ on: branches: [main] paths: - "*.Dockerfile" + - "docker-compose.selfhost.yml" + - "scripts/test-selfhost-init.sh" - ".dockerignore" - "apps/api/**" - "apps/basket/**" @@ -20,6 +22,8 @@ on: branches: [main, staging] paths: - "*.Dockerfile" + - "docker-compose.selfhost.yml" + - "scripts/test-selfhost-init.sh" - ".dockerignore" - "apps/api/**" - "apps/basket/**" @@ -40,6 +44,14 @@ concurrency: cancel-in-progress: true jobs: + selfhost-init: + name: Self-host Database Initialization + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - run: bash scripts/test-selfhost-init.sh + vector-config-check: name: Vector Delivery Canary runs-on: blacksmith-2vcpu-ubuntu-2404 diff --git a/README.md b/README.md index 64cc0c3fc6..859dd63142 100644 --- a/README.md +++ b/README.md @@ -77,21 +77,40 @@ Databuddy can be self-hosted using Docker Compose. The repo includes two compose cp .env.example .env # Edit .env — set IMAGE_TAG, URL-safe database/cache passwords, public URLs, # BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, IP_HASH_SALT, and -# AI_GATEWAY_API_KEY. Make the local database URLs use the same credentials -# before running the initialization commands below. +# AI_GATEWAY_API_KEY. # 2. Start databases and cache docker compose -f docker-compose.selfhost.yml up -d postgres clickhouse redis -# 3. Initialize databases from the repo checkout (first run only) -bun install --frozen-lockfile -bun run db:push -bun run clickhouse:init +# 3. Initialize databases using the matching release image +docker compose -f docker-compose.selfhost.yml run --rm init # 4. Start backend services docker compose -f docker-compose.selfhost.yml up -d ``` +The `init` service contains the schema source and tooling; the compiled API +image does not. It runs PostgreSQL `db:push`, then creates missing ClickHouse +tables and views. It only runs when explicitly requested. No local Bun install +or custom migration script is needed. + +For upgrades, back up your databases, set `IMAGE_TAG` to the new release, and +apply PostgreSQL changes separately so you can review any schema change prompts: + +```bash +docker compose -f docker-compose.selfhost.yml pull init +docker compose -f docker-compose.selfhost.yml run --rm init bun run --cwd packages/db db:push +``` + +If you decline a PostgreSQL change, stop the upgrade. After accepting the changes, +create any missing ClickHouse objects with +`docker compose -f docker-compose.selfhost.yml run --rm init bun --cwd packages/db src/clickhouse/setup.ts`. +This only creates missing objects; apply any additional migrations listed in the +release notes separately before starting the updated services. + +To verify a local init image against disposable databases, run +`bash scripts/test-selfhost-init.sh` (requires Docker Compose 2.24.4 or later). + Services started: - **API** → `localhost:3001` - **Basket** (event ingestion) → `localhost:4000` diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 324f667eaf..f595223483 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -44,9 +44,10 @@ services: soft: 262144 hard: 262144 healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping" ] + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8123/ping" ] interval: 10s timeout: 5s + start_period: 30s retries: 5 restart: unless-stopped networks: @@ -74,6 +75,22 @@ services: - databuddy <<: *logging + init: + image: ghcr.io/databuddy-analytics/databuddy-init:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} + profiles: [ "tools" ] + environment: + DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" + CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" + depends_on: + postgres: + condition: service_healthy + clickhouse: + condition: service_healthy + restart: "no" + networks: + - databuddy + <<: *logging + api: image: ghcr.io/databuddy-analytics/databuddy-api:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} container_name: databuddy-api diff --git a/init.Dockerfile b/init.Dockerfile index b9867b7d7d..edb1e4dcce 100644 --- a/init.Dockerfile +++ b/init.Dockerfile @@ -5,8 +5,9 @@ WORKDIR /app COPY package.json bun.lock turbo.json ./ COPY packages ./packages COPY apps ./apps +COPY tsconfig ./tsconfig -RUN bun install --ignore-scripts +RUN bun install --frozen-lockfile --ignore-scripts ENV NODE_ENV=production diff --git a/scripts/test-selfhost-init.sh b/scripts/test-selfhost-init.sh new file mode 100644 index 0000000000..c9307145af --- /dev/null +++ b/scripts/test-selfhost-init.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." +test_dir=$(mktemp -d) +project="databuddy-init-test-$$" +export IMAGE_TAG=selfhost-test +export POSTGRES_PASSWORD=init_test_password CLICKHOUSE_PASSWORD=init_test_password +export POSTGRES_USER=databuddy POSTGRES_DB=databuddy +export CLICKHOUSE_USER=default CLICKHOUSE_DB=databuddy_analytics +export REDIS_PASSWORD=unused BETTER_AUTH_SECRET=unused DATABUDDY_ENCRYPTION_KEY=unused +export IP_HASH_SALT=unused AI_GATEWAY_API_KEY=unused +export DASHBOARD_URL=http://example.com API_URL=http://api.example.com BASKET_URL=http://basket.example.com + +cat > "$test_dir/compose.yml" <&2 + exit 1 +fi +echo "Self-host initialization smoke test passed" From b4e842060f366d04424ef27eaeb208819d3b5bd3 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Tue, 15 Sep 2026 22:53:28 +0300 Subject: [PATCH 2/9] fix(ci): simplify self-host Docker startup --- README.md | 46 +++++-- dashboard.Dockerfile | 2 + docker-compose.selfhost.yml | 222 +++++++++++++++------------------- infra/railway-template.md | 2 - scripts/test-selfhost-init.sh | 4 +- 5 files changed, 135 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 859dd63142..520b588960 100644 --- a/README.md +++ b/README.md @@ -68,16 +68,21 @@ Databuddy can be self-hosted using Docker Compose. The repo includes two compose | File | Purpose | |---|---| | `docker-compose.yaml` | **Development only** — starts infrastructure (Postgres, ClickHouse, Redis) for local dev | -| `docker-compose.selfhost.yml` | **Production / self-hosting** — backend services from GHCR images | +| `docker-compose.selfhost.yml` | **Self-hosting** — backend images plus a dashboard built for your URLs | ### Quick Start +Use a checkout matching `IMAGE_TAG`. Docker Compose is sufficient; Bun and Node +are only needed for local development. For local testing, use +`http://localhost:3000`, `http://localhost:3001`, and `http://localhost:4000` +for the dashboard, API, and Basket URLs. + ```bash # 1. Configure environment cp .env.example .env -# Edit .env — set IMAGE_TAG, URL-safe database/cache passwords, public URLs, -# BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, IP_HASH_SALT, and -# AI_GATEWAY_API_KEY. +# Edit .env — set IMAGE_TAG, POSTGRES_PASSWORD, CLICKHOUSE_PASSWORD, +# REDIS_PASSWORD, BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, and the +# DASHBOARD_URL, API_URL, BASKET_URL public URLs. Use URL-safe passwords. # 2. Start databases and cache docker compose -f docker-compose.selfhost.yml up -d postgres clickhouse redis @@ -85,8 +90,8 @@ docker compose -f docker-compose.selfhost.yml up -d postgres clickhouse redis # 3. Initialize databases using the matching release image docker compose -f docker-compose.selfhost.yml run --rm init -# 4. Start backend services -docker compose -f docker-compose.selfhost.yml up -d +# 4. Build the dashboard for your URLs and start the services +docker compose -f docker-compose.selfhost.yml up -d --build ``` The `init` service contains the schema source and tooling; the compiled API @@ -94,8 +99,9 @@ image does not. It runs PostgreSQL `db:push`, then creates missing ClickHouse tables and views. It only runs when explicitly requested. No local Bun install or custom migration script is needed. -For upgrades, back up your databases, set `IMAGE_TAG` to the new release, and -apply PostgreSQL changes separately so you can review any schema change prompts: +For upgrades, back up your databases, check out the new release, and set +`IMAGE_TAG` to that release. Apply PostgreSQL changes separately so you can review +any schema change prompts: ```bash docker compose -f docker-compose.selfhost.yml pull init @@ -112,12 +118,32 @@ To verify a local init image against disposable databases, run `bash scripts/test-selfhost-init.sh` (requires Docker Compose 2.24.4 or later). Services started: +- **Dashboard** → `localhost:3000` - **API** → `localhost:3001` - **Basket** (event ingestion) → `localhost:4000` -- **Insights** (investigation worker) → `localhost:4002` - **Links** (short links) → `localhost:2500` -All ports are configurable via env vars (`API_PORT`, `BASKET_PORT`, etc.). See the compose file comments for the full env var reference. +Ports are configurable (`DASHBOARD_PORT`, `API_PORT`, `BASKET_PORT`, `LINKS_PORT`). +For remote access, put the dashboard and API behind HTTPS on the same parent +domain and set `BETTER_AUTH_COOKIE_DOMAIN` (for example `.example.com`) so login +works across subdomains. Leave it empty for localhost. Rebuild the dashboard +with `docker compose -f docker-compose.selfhost.yml up -d --build` after changing +public URLs; they are embedded in its browser bundle. + +### Optional services + +Email is optional for self-host signup. For password resets, invitations, and +alerts, set `RESEND_API_KEY` and `EMAIL_FROM` to a sender on your verified domain, +for example `Databuddy `. Leave `ALERTS_EMAIL_FROM` empty to +reuse that sender. Recreate services after changing these values. + +Insights is opt-in: configure its AI and billing providers, then run +`docker compose -f docker-compose.selfhost.yml --profile insights up -d insights`. +Basic analytics and link delivery do not require an AI key or Kafka. +Error analytics and creating goals, funnels, or feature flags still require an +Autumn billing provider configuration. The billing UI can show errors without it. +DQL requires separate restricted-user provisioning; never use the application's +admin ClickHouse credentials for DQL. ## 🤝 Contributing diff --git a/dashboard.Dockerfile b/dashboard.Dockerfile index bbddba2fc9..d612882c80 100644 --- a/dashboard.Dockerfile +++ b/dashboard.Dockerfile @@ -19,6 +19,8 @@ COPY turbo.json turbo.json ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +ARG SELFHOST=false +ENV SELFHOST=$SELFHOST # Build-time defaults keep the image buildable. Override these with real public # URLs when building environment-specific dashboard images. ARG NEXT_PUBLIC_API_URL=https://api.databuddy.cc diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index f595223483..5303c79095 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -5,14 +5,50 @@ x-logging: &logging max-size: "10m" max-file: "3" +x-environment: &environment + NODE_ENV: production + SELFHOST: "true" + DATABASE_URL: &database-url "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-databuddy}" + REDIS_URL: &redis-url "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD}@redis:6379" + BULLMQ_REDIS_URL: *redis-url + CLICKHOUSE_URL: &clickhouse-url "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" + DB_POOL_MAX: ${DB_POOL_MAX:-10} + DASHBOARD_URL: &dashboard-url ${DASHBOARD_URL:?Set DASHBOARD_URL} + +x-auth-environment: &auth-environment + <<: *environment + BETTER_AUTH_URL: *dashboard-url + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET} + BETTER_AUTH_COOKIE_DOMAIN: ${BETTER_AUTH_COOKIE_DOMAIN:-} + DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY} + API_URL: &api-url ${API_URL:?Set API_URL} + BASKET_URL: &basket-url ${BASKET_URL:?Set BASKET_URL} + LINKS_URL: &links-url ${LINKS_URL:-http://localhost:2500} + STATUS_URL: &status-url ${STATUS_URL:-http://localhost:3002} + EMAIL_FROM: ${EMAIL_FROM:-} + ALERTS_EMAIL_FROM: ${ALERTS_EMAIL_FROM:-} + RESEND_API_KEY: ${RESEND_API_KEY:-} + AUTUMN_SECRET_KEY: ${AUTUMN_SECRET_KEY:-} + +x-backend: &backend + <<: *logging + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + clickhouse: + condition: service_healthy + redis: + condition: service_healthy + services: postgres: + <<: *logging image: postgres:17-alpine - container_name: databuddy-postgres environment: POSTGRES_DB: ${POSTGRES_DB:-databuddy} POSTGRES_USER: ${POSTGRES_USER:-databuddy} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD} ports: - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" volumes: @@ -23,17 +59,14 @@ services: timeout: 5s retries: 5 restart: unless-stopped - networks: - - databuddy - <<: *logging clickhouse: + <<: *logging image: clickhouse/clickhouse-server:25.5.1-alpine - container_name: databuddy-clickhouse environment: CLICKHOUSE_DB: ${CLICKHOUSE_DB:-databuddy_analytics} CLICKHOUSE_USER: ${CLICKHOUSE_USER:-default} - CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment} + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD} CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 ports: - "127.0.0.1:${CLICKHOUSE_PORT:-8123}:8123" @@ -50,209 +83,146 @@ services: start_period: 30s retries: 5 restart: unless-stopped - networks: - - databuddy - <<: *logging redis: + <<: *logging image: redis:7-alpine - container_name: databuddy-redis ports: - "127.0.0.1:${REDIS_PORT:-6379}:6379" volumes: - redis_data:/data environment: - REDISCLI_AUTH: ${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment} - command: > - redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass ${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment} + REDISCLI_AUTH: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} + command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy noeviction --requirepass ${REDIS_PASSWORD:?Set REDIS_PASSWORD} healthcheck: test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 5s retries: 5 restart: unless-stopped - networks: - - databuddy - <<: *logging init: - image: ghcr.io/databuddy-analytics/databuddy-init:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} + <<: *logging + image: ghcr.io/databuddy-analytics/databuddy-init:${IMAGE_TAG:?Set IMAGE_TAG to a release tag} profiles: [ "tools" ] environment: - DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" - CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" + DATABASE_URL: *database-url + CLICKHOUSE_URL: *clickhouse-url depends_on: postgres: condition: service_healthy clickhouse: condition: service_healthy restart: "no" - networks: - - databuddy - <<: *logging + + dashboard: + <<: *backend + build: + context: . + dockerfile: dashboard.Dockerfile + args: + SELFHOST: "true" + NEXT_PUBLIC_APP_URL: *dashboard-url + NEXT_PUBLIC_API_URL: *api-url + NEXT_PUBLIC_BASKET_URL: *basket-url + NEXT_PUBLIC_LINKS_URL: *links-url + NEXT_PUBLIC_STATUS_URL: *status-url + ports: + - "${DASHBOARD_PORT:-3000}:3000" + environment: *auth-environment + healthcheck: + test: [ "CMD", "node", "-e", "fetch('http://127.0.0.1:3000/login').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] + interval: 30s + timeout: 3s + start_period: 30s + retries: 3 api: - image: ghcr.io/databuddy-analytics/databuddy-api:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} - container_name: databuddy-api + <<: *backend + image: ghcr.io/databuddy-analytics/databuddy-api:${IMAGE_TAG:?Set IMAGE_TAG to a release tag} ports: - "${API_PORT:-3001}:3001" environment: - NODE_ENV: production + <<: *auth-environment PORT: "3001" - DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" - DB_POOL_MAX: ${DB_POOL_MAX:-10} SERVICE_NAME: databuddy-api - REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - BULLMQ_REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" - BETTER_AUTH_URL: ${DASHBOARD_URL:?Set DASHBOARD_URL in your environment} - BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in your environment} - DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY in your environment} - SELFHOST: "true" - DASHBOARD_URL: ${DASHBOARD_URL:?Set DASHBOARD_URL in your environment} - API_URL: ${API_URL:?Set API_URL in your environment} - BASKET_URL: ${BASKET_URL:?Set BASKET_URL in your environment} - EMAIL_FROM: ${EMAIL_FROM:-} - ALERTS_EMAIL_FROM: ${ALERTS_EMAIL_FROM:-} AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:-} SUPERMEMORY_API_KEY: ${SUPERMEMORY_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} - RESEND_API_KEY: ${RESEND_API_KEY:-} healthcheck: - test: [ "CMD", "bun", "-e", "fetch('http://localhost:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] + test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] interval: 30s timeout: 3s start_period: 10s retries: 3 - depends_on: - postgres: - condition: service_healthy - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped - networks: - - databuddy - <<: *logging basket: - image: ghcr.io/databuddy-analytics/databuddy-basket:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} - container_name: databuddy-basket + <<: *backend + image: ghcr.io/databuddy-analytics/databuddy-basket:${IMAGE_TAG:?Set IMAGE_TAG to a release tag} ports: - "${BASKET_PORT:-4000}:4000" environment: - NODE_ENV: production + <<: *environment PORT: "4000" - DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" - DB_POOL_MAX: ${DB_POOL_MAX:-10} SERVICE_NAME: databuddy-basket - REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" - DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY in your environment} - IP_HASH_SALT: ${IP_HASH_SALT:?Set IP_HASH_SALT in your environment} + DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY} + EMAIL_FROM: ${EMAIL_FROM:-} + ALERTS_EMAIL_FROM: ${ALERTS_EMAIL_FROM:-} + RESEND_API_KEY: ${RESEND_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} - SELFHOST: "true" healthcheck: - test: [ "CMD", "bun", "-e", "fetch('http://localhost:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] + test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] interval: 30s timeout: 3s start_period: 10s retries: 3 - depends_on: - postgres: - condition: service_healthy - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped - networks: - - databuddy - <<: *logging insights: - image: ghcr.io/databuddy-analytics/databuddy-insights:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} - container_name: databuddy-insights + <<: *backend + image: ghcr.io/databuddy-analytics/databuddy-insights:${IMAGE_TAG:?Set IMAGE_TAG to a release tag} + profiles: [ "insights" ] ports: - "${INSIGHTS_PORT:-4002}:4002" environment: - NODE_ENV: production + <<: *environment PORT: "4002" - DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" - DB_POOL_MAX: ${DB_POOL_MAX:-10} SERVICE_NAME: databuddy-insights - REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - BULLMQ_REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" INSIGHTS_WORKER_CONCURRENCY: ${INSIGHTS_WORKER_CONCURRENCY:-2} - INSIGHTS_WORKER_ENABLED: "true" - AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:?Set AI_GATEWAY_API_KEY in your environment} + INSIGHTS_WORKER_ENABLED: "${AI_GATEWAY_API_KEY:+true}" + AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} + AUTUMN_SECRET_KEY: ${AUTUMN_SECRET_KEY:-} FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:-} SUPERMEMORY_API_KEY: ${SUPERMEMORY_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} - CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" - DASHBOARD_URL: ${DASHBOARD_URL:?Set DASHBOARD_URL in your environment} - DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY in your environment} - BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in your environment} + DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY} + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET} healthcheck: - test: [ "CMD", "bun", "-e", "fetch('http://localhost:4002/health/status').then(async r=>process.exit(r.ok&&(await r.json()).status==='ok'?0:1)).catch(()=>process.exit(1))" ] + test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:4002/health/status').then(async r=>process.exit(r.ok&&(await r.json()).status==='ok'?0:1)).catch(()=>process.exit(1))" ] interval: 30s timeout: 3s start_period: 10s retries: 3 - depends_on: - postgres: - condition: service_healthy - clickhouse: - condition: service_healthy - redis: - condition: service_healthy - restart: unless-stopped - networks: - - databuddy - <<: *logging links: - image: ghcr.io/databuddy-analytics/databuddy-links:${IMAGE_TAG:?Set IMAGE_TAG to a release tag, for example v1.0.0} - container_name: databuddy-links + <<: *backend + image: ghcr.io/databuddy-analytics/databuddy-links:${IMAGE_TAG:?Set IMAGE_TAG to a release tag} ports: - "${LINKS_PORT:-2500}:2500" environment: - NODE_ENV: production - DATABASE_URL: "postgres://${POSTGRES_USER:-databuddy}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in your environment}@postgres:5432/${POSTGRES_DB:-databuddy}" - DB_POOL_MAX: ${DB_POOL_MAX:-10} + <<: *environment SERVICE_NAME: databuddy-links - REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - BULLMQ_REDIS_URL: "redis://:${REDIS_PASSWORD:?Set REDIS_PASSWORD in your environment}@redis:6379" - CLICKHOUSE_URL: "http://${CLICKHOUSE_USER:-default}:${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in your environment}@clickhouse:8123/${CLICKHOUSE_DB:-databuddy_analytics}" - DASHBOARD_URL: ${DASHBOARD_URL:?Set DASHBOARD_URL in your environment} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} LINKS_ROOT_REDIRECT_URL: ${LINKS_ROOT_REDIRECT_URL:-https://databuddy.cc} GEOIP_DB_URL: ${GEOIP_DB_URL:-https://cdn.databuddy.cc/mmdb/GeoLite2-City.mmdb} healthcheck: - test: [ "CMD", "bun", "-e", "fetch('http://localhost:2500/health/status').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] + test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:2500/health/status').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] interval: 30s timeout: 3s start_period: 10s retries: 3 - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - clickhouse: - condition: service_healthy - restart: unless-stopped - networks: - - databuddy - <<: *logging + volumes: postgres_data: clickhouse_data: redis_data: - - -networks: - databuddy: - driver: bridge diff --git a/infra/railway-template.md b/infra/railway-template.md index bcbb22b35c..5ac223d065 100644 --- a/infra/railway-template.md +++ b/infra/railway-template.md @@ -54,7 +54,6 @@ NODE_ENV=production SELFHOST=true BETTER_AUTH_SECRET=${{secret(64)}} DATABUDDY_ENCRYPTION_KEY=${{secret(64)}} -IP_HASH_SALT=${{secret(64)}} AI_GATEWAY_API_KEY= ``` @@ -124,7 +123,6 @@ DATABASE_URL=${{Postgres.DATABASE_URL}} REDIS_URL=${{Redis.REDIS_URL}} CLICKHOUSE_URL=${{ClickHouse.DATABASE_URL}} DATABUDDY_ENCRYPTION_KEY=${{shared.DATABUDDY_ENCRYPTION_KEY}} -IP_HASH_SALT=${{shared.IP_HASH_SALT}} SELFHOST=${{shared.SELFHOST}} ``` diff --git a/scripts/test-selfhost-init.sh b/scripts/test-selfhost-init.sh index c9307145af..c504a1a0ec 100644 --- a/scripts/test-selfhost-init.sh +++ b/scripts/test-selfhost-init.sh @@ -9,16 +9,14 @@ export POSTGRES_PASSWORD=init_test_password CLICKHOUSE_PASSWORD=init_test_passwo export POSTGRES_USER=databuddy POSTGRES_DB=databuddy export CLICKHOUSE_USER=default CLICKHOUSE_DB=databuddy_analytics export REDIS_PASSWORD=unused BETTER_AUTH_SECRET=unused DATABUDDY_ENCRYPTION_KEY=unused -export IP_HASH_SALT=unused AI_GATEWAY_API_KEY=unused +unset AI_GATEWAY_API_KEY export DASHBOARD_URL=http://example.com API_URL=http://api.example.com BASKET_URL=http://basket.example.com cat > "$test_dir/compose.yml" < Date: Tue, 15 Sep 2026 23:09:12 +0300 Subject: [PATCH 3/9] refactor(ci): trim self-host setup repetition --- README.md | 18 ++++++------------ docker-compose.selfhost.yml | 17 ++++------------- scripts/test-selfhost-init.sh | 9 ++++----- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 520b588960..0d1278039b 100644 --- a/README.md +++ b/README.md @@ -84,20 +84,15 @@ cp .env.example .env # REDIS_PASSWORD, BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, and the # DASHBOARD_URL, API_URL, BASKET_URL public URLs. Use URL-safe passwords. -# 2. Start databases and cache -docker compose -f docker-compose.selfhost.yml up -d postgres clickhouse redis - -# 3. Initialize databases using the matching release image +# 2. Start databases and initialize their schemas docker compose -f docker-compose.selfhost.yml run --rm init -# 4. Build the dashboard for your URLs and start the services +# 3. Build the dashboard for your URLs and start the services docker compose -f docker-compose.selfhost.yml up -d --build ``` -The `init` service contains the schema source and tooling; the compiled API -image does not. It runs PostgreSQL `db:push`, then creates missing ClickHouse -tables and views. It only runs when explicitly requested. No local Bun install -or custom migration script is needed. +The explicit `init` command runs PostgreSQL `db:push`, then creates missing +ClickHouse tables and views using the release's schema source and tooling. For upgrades, back up your databases, check out the new release, and set `IMAGE_TAG` to that release. Apply PostgreSQL changes separately so you can review @@ -126,9 +121,8 @@ Services started: Ports are configurable (`DASHBOARD_PORT`, `API_PORT`, `BASKET_PORT`, `LINKS_PORT`). For remote access, put the dashboard and API behind HTTPS on the same parent domain and set `BETTER_AUTH_COOKIE_DOMAIN` (for example `.example.com`) so login -works across subdomains. Leave it empty for localhost. Rebuild the dashboard -with `docker compose -f docker-compose.selfhost.yml up -d --build` after changing -public URLs; they are embedded in its browser bundle. +works across subdomains. Leave it empty for localhost. Repeat step 3 after changing +public URLs; they are embedded in the dashboard's browser bundle. ### Optional services diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 5303c79095..4cc512d5d6 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -150,7 +150,7 @@ services: FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:-} SUPERMEMORY_API_KEY: ${SUPERMEMORY_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} - healthcheck: + healthcheck: &http-healthcheck test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:3001/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] interval: 30s timeout: 3s @@ -172,11 +172,8 @@ services: RESEND_API_KEY: ${RESEND_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} healthcheck: + <<: *http-healthcheck test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] - interval: 30s - timeout: 3s - start_period: 10s - retries: 3 insights: <<: *backend @@ -198,11 +195,8 @@ services: DATABUDDY_ENCRYPTION_KEY: ${DATABUDDY_ENCRYPTION_KEY:?Set DATABUDDY_ENCRYPTION_KEY} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET} healthcheck: + <<: *http-healthcheck test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:4002/health/status').then(async r=>process.exit(r.ok&&(await r.json()).status==='ok'?0:1)).catch(()=>process.exit(1))" ] - interval: 30s - timeout: 3s - start_period: 10s - retries: 3 links: <<: *backend @@ -216,11 +210,8 @@ services: LINKS_ROOT_REDIRECT_URL: ${LINKS_ROOT_REDIRECT_URL:-https://databuddy.cc} GEOIP_DB_URL: ${GEOIP_DB_URL:-https://cdn.databuddy.cc/mmdb/GeoLite2-City.mmdb} healthcheck: + <<: *http-healthcheck test: [ "CMD", "bun", "-e", "fetch('http://127.0.0.1:2500/health/status').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" ] - interval: 30s - timeout: 3s - start_period: 10s - retries: 3 volumes: postgres_data: diff --git a/scripts/test-selfhost-init.sh b/scripts/test-selfhost-init.sh index c504a1a0ec..2f7e84dc66 100644 --- a/scripts/test-selfhost-init.sh +++ b/scripts/test-selfhost-init.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "$0")/.." -test_dir=$(mktemp -d) +test_compose=$(mktemp) project="databuddy-init-test-$$" export IMAGE_TAG=selfhost-test export POSTGRES_PASSWORD=init_test_password CLICKHOUSE_PASSWORD=init_test_password @@ -12,7 +12,7 @@ export REDIS_PASSWORD=unused BETTER_AUTH_SECRET=unused DATABUDDY_ENCRYPTION_KEY= unset AI_GATEWAY_API_KEY export DASHBOARD_URL=http://example.com API_URL=http://api.example.com BASKET_URL=http://basket.example.com -cat > "$test_dir/compose.yml" < "$test_compose" <<'EOF' services: postgres: ports: !reset [] @@ -25,12 +25,11 @@ EOF compose() { docker compose --project-name "$project" --env-file /dev/null \ - -f docker-compose.selfhost.yml -f "$test_dir/compose.yml" "$@" + -f docker-compose.selfhost.yml -f "$test_compose" "$@" } cleanup() { compose down --volumes --remove-orphans - rm -f "$test_dir/compose.yml" - rmdir "$test_dir" + rm -f "$test_compose" } trap cleanup EXIT From 8e5d91837eb3dda2dc77ec6060d9fe82ec1b3013 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:08:32 +0300 Subject: [PATCH 4/9] docs(ci): move onboarding copy to its own PR --- README.md | 69 ++++++++++++------------------------------------------- 1 file changed, 15 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 0d1278039b..64cc0c3fc6 100644 --- a/README.md +++ b/README.md @@ -68,76 +68,37 @@ Databuddy can be self-hosted using Docker Compose. The repo includes two compose | File | Purpose | |---|---| | `docker-compose.yaml` | **Development only** — starts infrastructure (Postgres, ClickHouse, Redis) for local dev | -| `docker-compose.selfhost.yml` | **Self-hosting** — backend images plus a dashboard built for your URLs | +| `docker-compose.selfhost.yml` | **Production / self-hosting** — backend services from GHCR images | ### Quick Start -Use a checkout matching `IMAGE_TAG`. Docker Compose is sufficient; Bun and Node -are only needed for local development. For local testing, use -`http://localhost:3000`, `http://localhost:3001`, and `http://localhost:4000` -for the dashboard, API, and Basket URLs. - ```bash # 1. Configure environment cp .env.example .env -# Edit .env — set IMAGE_TAG, POSTGRES_PASSWORD, CLICKHOUSE_PASSWORD, -# REDIS_PASSWORD, BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, and the -# DASHBOARD_URL, API_URL, BASKET_URL public URLs. Use URL-safe passwords. - -# 2. Start databases and initialize their schemas -docker compose -f docker-compose.selfhost.yml run --rm init - -# 3. Build the dashboard for your URLs and start the services -docker compose -f docker-compose.selfhost.yml up -d --build -``` +# Edit .env — set IMAGE_TAG, URL-safe database/cache passwords, public URLs, +# BETTER_AUTH_SECRET, DATABUDDY_ENCRYPTION_KEY, IP_HASH_SALT, and +# AI_GATEWAY_API_KEY. Make the local database URLs use the same credentials +# before running the initialization commands below. -The explicit `init` command runs PostgreSQL `db:push`, then creates missing -ClickHouse tables and views using the release's schema source and tooling. +# 2. Start databases and cache +docker compose -f docker-compose.selfhost.yml up -d postgres clickhouse redis -For upgrades, back up your databases, check out the new release, and set -`IMAGE_TAG` to that release. Apply PostgreSQL changes separately so you can review -any schema change prompts: +# 3. Initialize databases from the repo checkout (first run only) +bun install --frozen-lockfile +bun run db:push +bun run clickhouse:init -```bash -docker compose -f docker-compose.selfhost.yml pull init -docker compose -f docker-compose.selfhost.yml run --rm init bun run --cwd packages/db db:push +# 4. Start backend services +docker compose -f docker-compose.selfhost.yml up -d ``` -If you decline a PostgreSQL change, stop the upgrade. After accepting the changes, -create any missing ClickHouse objects with -`docker compose -f docker-compose.selfhost.yml run --rm init bun --cwd packages/db src/clickhouse/setup.ts`. -This only creates missing objects; apply any additional migrations listed in the -release notes separately before starting the updated services. - -To verify a local init image against disposable databases, run -`bash scripts/test-selfhost-init.sh` (requires Docker Compose 2.24.4 or later). - Services started: -- **Dashboard** → `localhost:3000` - **API** → `localhost:3001` - **Basket** (event ingestion) → `localhost:4000` +- **Insights** (investigation worker) → `localhost:4002` - **Links** (short links) → `localhost:2500` -Ports are configurable (`DASHBOARD_PORT`, `API_PORT`, `BASKET_PORT`, `LINKS_PORT`). -For remote access, put the dashboard and API behind HTTPS on the same parent -domain and set `BETTER_AUTH_COOKIE_DOMAIN` (for example `.example.com`) so login -works across subdomains. Leave it empty for localhost. Repeat step 3 after changing -public URLs; they are embedded in the dashboard's browser bundle. - -### Optional services - -Email is optional for self-host signup. For password resets, invitations, and -alerts, set `RESEND_API_KEY` and `EMAIL_FROM` to a sender on your verified domain, -for example `Databuddy `. Leave `ALERTS_EMAIL_FROM` empty to -reuse that sender. Recreate services after changing these values. - -Insights is opt-in: configure its AI and billing providers, then run -`docker compose -f docker-compose.selfhost.yml --profile insights up -d insights`. -Basic analytics and link delivery do not require an AI key or Kafka. -Error analytics and creating goals, funnels, or feature flags still require an -Autumn billing provider configuration. The billing UI can show errors without it. -DQL requires separate restricted-user provisioning; never use the application's -admin ClickHouse credentials for DQL. +All ports are configurable via env vars (`API_PORT`, `BASKET_PORT`, etc.). See the compose file comments for the full env var reference. ## 🤝 Contributing From 935cf65e7a45e64a92f2c2078ca5338b7ddf9b9a Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:02:40 +0300 Subject: [PATCH 5/9] fix(ci): stop forwarding billing credentials to self-hosted services --- docker-compose.selfhost.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 4cc512d5d6..43d63ff8c8 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -28,7 +28,6 @@ x-auth-environment: &auth-environment EMAIL_FROM: ${EMAIL_FROM:-} ALERTS_EMAIL_FROM: ${ALERTS_EMAIL_FROM:-} RESEND_API_KEY: ${RESEND_API_KEY:-} - AUTUMN_SECRET_KEY: ${AUTUMN_SECRET_KEY:-} x-backend: &backend <<: *logging @@ -188,7 +187,6 @@ services: INSIGHTS_WORKER_CONCURRENCY: ${INSIGHTS_WORKER_CONCURRENCY:-2} INSIGHTS_WORKER_ENABLED: "${AI_GATEWAY_API_KEY:+true}" AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} - AUTUMN_SECRET_KEY: ${AUTUMN_SECRET_KEY:-} FIRECRAWL_API_KEY: ${FIRECRAWL_API_KEY:-} SUPERMEMORY_API_KEY: ${SUPERMEMORY_API_KEY:-} SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} From 5c6029cd084c733104235b25c97684836a42c3d6 Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:21:27 +0300 Subject: [PATCH 6/9] fix(ci): simplify self-host environment setup --- .env.example | 11 +++-------- docker-compose.selfhost.yml | 2 +- selfhost.env.example | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 selfhost.env.example diff --git a/.env.example b/.env.example index 6e39da10c3..9bd79356bd 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ +# Local development. For Docker self-hosting, use selfhost.env.example. + CLICKHOUSE_URL="http://default:@localhost:8123/databuddy_analytics" # Required by DQL. This must use the separately provisioned dql_user account; # never point it at the application/admin ClickHouse credentials. @@ -6,13 +8,6 @@ DATABASE_URL="postgres://databuddy:databuddy_dev_password@localhost:5432/databud REDIS_URL="redis://localhost:6379" BULLMQ_REDIS_URL="redis://localhost:6379" -# Required only by docker-compose.selfhost.yml. Use URL-safe passwords. -# Compose configures the container database URLs from these values. -IMAGE_TAG="" -POSTGRES_PASSWORD="" -CLICKHOUSE_PASSWORD="" -REDIS_PASSWORD="" - # Public URLs. Local development uses built-in localhost defaults when blank. DASHBOARD_URL="" API_URL="" @@ -31,7 +26,7 @@ FIRECRAWL_API_KEY="" SUPERMEMORY_API_KEY="" BETTER_AUTH_URL="http://localhost:3000" -# Required by docker-compose.selfhost.yml. Generate independent random values. +# Generate independent random values for sessions and encryption. BETTER_AUTH_SECRET="generate-a-random-32-byte-base64-secret" DATABUDDY_ENCRYPTION_KEY="" diff --git a/docker-compose.selfhost.yml b/docker-compose.selfhost.yml index 43d63ff8c8..e0b1477155 100644 --- a/docker-compose.selfhost.yml +++ b/docker-compose.selfhost.yml @@ -205,7 +205,7 @@ services: <<: *environment SERVICE_NAME: databuddy-links SUPERLOG_API_KEY: ${SUPERLOG_API_KEY:-} - LINKS_ROOT_REDIRECT_URL: ${LINKS_ROOT_REDIRECT_URL:-https://databuddy.cc} + LINKS_ROOT_REDIRECT_URL: ${LINKS_ROOT_REDIRECT_URL:-${DASHBOARD_URL}} GEOIP_DB_URL: ${GEOIP_DB_URL:-https://cdn.databuddy.cc/mmdb/GeoLite2-City.mmdb} healthcheck: <<: *http-healthcheck diff --git a/selfhost.env.example b/selfhost.env.example new file mode 100644 index 0000000000..8c80509fd2 --- /dev/null +++ b/selfhost.env.example @@ -0,0 +1,26 @@ +# Copy to .env. Use the same release tag as your checkout. +IMAGE_TAG= + +# Generate each value separately with: openssl rand -hex 32 +POSTGRES_PASSWORD= +CLICKHOUSE_PASSWORD= +REDIS_PASSWORD= +BETTER_AUTH_SECRET= +DATABUDDY_ENCRYPTION_KEY= + +# Local defaults. For a public instance, use your HTTPS URLs. +DASHBOARD_URL=http://localhost:3000 +API_URL=http://localhost:3001 +BASKET_URL=http://localhost:4000 +LINKS_URL=http://localhost:2500 + +# Shared dashboard/API parent domain (e.g. .example.com); empty for localhost. +BETTER_AUTH_COOKIE_DOMAIN= + +# Optional email: set both values and use a verified sender domain. +# RESEND_API_KEY= +# EMAIL_FROM="Databuddy " + +# Optional AI: README.md#optional-services +# AI_GATEWAY_API_KEY= +# COMPOSE_PROFILES=insights From 8c0b8b9f536a5dbce68ffa6ab21d4842c8a6fb3a Mon Sep 17 00:00:00 2001 From: iza <59828082+izadoesdev@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:21:54 +0300 Subject: [PATCH 7/9] fix(dashboard): hide unconfigured self-host status links --- .github/workflows/ci.yml | 1 + .github/workflows/health-check.yml | 4 ++ .../monitors/status-pages/[id]/page.tsx | 25 ++++++----- .../status-pages/status-page-row.tsx | 42 ++++++++++--------- apps/dashboard/lib/app-url.test.ts | 26 ++++++++++++ apps/dashboard/lib/app-url.ts | 6 ++- docker-compose.selfhost.yml | 2 +- scripts/test-selfhost-init.sh | 23 ++++++---- selfhost.env.example | 3 ++ 9 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 apps/dashboard/lib/app-url.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86dd13240c..37f79fb5fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,7 @@ jobs: run: >- bun test --no-env-file apps/dashboard/next-config.test.ts apps/dashboard/app/layout.test.ts + apps/dashboard/lib/app-url.test.ts 'apps/dashboard/app/(main)/websites/[id]/_components/utils/code-generators.test.ts' 'apps/dashboard/app/(main)/onboarding/_components/step-install-tracking.test.ts' packages/auth/src/selfhost-config.test.ts diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml index e56c5ef4bb..a9732ed045 100644 --- a/.github/workflows/health-check.yml +++ b/.github/workflows/health-check.yml @@ -6,6 +6,7 @@ on: paths: - "*.Dockerfile" - "docker-compose.selfhost.yml" + - "selfhost.env.example" - "scripts/test-selfhost-init.sh" - ".dockerignore" - "apps/api/**" @@ -17,12 +18,14 @@ on: - "bun.lock" - "package.json" - "turbo.json" + - "tsconfig/**" - ".github/workflows/health-check.yml" pull_request: branches: [main, staging] paths: - "*.Dockerfile" - "docker-compose.selfhost.yml" + - "selfhost.env.example" - "scripts/test-selfhost-init.sh" - ".dockerignore" - "apps/api/**" @@ -34,6 +37,7 @@ on: - "bun.lock" - "package.json" - "turbo.json" + - "tsconfig/**" - ".github/workflows/health-check.yml" permissions: diff --git a/apps/dashboard/app/(main)/monitors/status-pages/[id]/page.tsx b/apps/dashboard/app/(main)/monitors/status-pages/[id]/page.tsx index 20632444ef..ea3755fbf4 100644 --- a/apps/dashboard/app/(main)/monitors/status-pages/[id]/page.tsx +++ b/apps/dashboard/app/(main)/monitors/status-pages/[id]/page.tsx @@ -76,6 +76,7 @@ export default function StatusPageDetailsPage() { }); const statusPage = statusPageQuery.data; + const statusPageUrl = statusPage && getStatusPageUrl(statusPage.slug); const monitorToRemoveData = statusPage?.monitors.find( (m: StatusPageMonitor) => m.id === monitorToRemove @@ -215,17 +216,19 @@ export default function StatusPageDetailsPage() {
{statusPage ? ( <> - - View Page - + {statusPageUrl && ( + + View Page + + )}