From 116e403f7a8a2e248ec78e9e7a22f9aeeb9e1e24 Mon Sep 17 00:00:00 2001 From: claw Date: Mon, 30 Mar 2026 22:52:14 +0200 Subject: [PATCH] refactor(deploy): pivot compose target to coolify Co-authored-by: claw --- .github/workflows/cd-vps.yml | 194 ------------------------ deploy/coolify/compose.yml | 102 +++++++++++++ deploy/vps/Caddyfile | 9 -- deploy/vps/bot.env.example | 39 ----- deploy/vps/caddy.env.example | 2 - deploy/vps/compose.yml | 74 --------- deploy/vps/miniapp.env.example | 1 - docs/runbooks/coolify-compose-deploy.md | 126 +++++++++++++++ docs/runbooks/vps-compose-deploy.md | 182 ---------------------- scripts/ops/vps-bootstrap-ubuntu.sh | 27 ---- scripts/ops/vps-deploy.sh | 61 -------- 11 files changed, 228 insertions(+), 589 deletions(-) delete mode 100644 .github/workflows/cd-vps.yml create mode 100644 deploy/coolify/compose.yml delete mode 100644 deploy/vps/Caddyfile delete mode 100644 deploy/vps/bot.env.example delete mode 100644 deploy/vps/caddy.env.example delete mode 100644 deploy/vps/compose.yml delete mode 100644 deploy/vps/miniapp.env.example create mode 100644 docs/runbooks/coolify-compose-deploy.md delete mode 100644 docs/runbooks/vps-compose-deploy.md delete mode 100755 scripts/ops/vps-bootstrap-ubuntu.sh delete mode 100755 scripts/ops/vps-deploy.sh diff --git a/.github/workflows/cd-vps.yml b/.github/workflows/cd-vps.yml deleted file mode 100644 index 58c309e..0000000 --- a/.github/workflows/cd-vps.yml +++ /dev/null @@ -1,194 +0,0 @@ -name: CD / VPS - -on: - workflow_run: - workflows: - - CI - types: - - completed - branches: - - main - workflow_dispatch: - inputs: - ref: - description: 'Git ref to deploy (branch, tag, or SHA)' - required: true - default: 'main' - -permissions: - contents: read - packages: write - -concurrency: - group: cd-vps-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref_name }} - cancel-in-progress: false - -jobs: - detect: - runs-on: ubuntu-latest - outputs: - eligible: ${{ steps.detect.outputs.eligible }} - ref: ${{ steps.detect.outputs.ref }} - steps: - - id: detect - run: | - eligible=false - ref="" - - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - eligible=true - ref="${{ inputs.ref }}" - elif [[ "${{ github.event_name }}" == "workflow_run" && "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then - eligible=true - ref="${{ github.event.workflow_run.head_sha }}" - fi - - echo "eligible=$eligible" >> "$GITHUB_OUTPUT" - echo "ref=$ref" >> "$GITHUB_OUTPUT" - - build-bot: - runs-on: ubuntu-latest - needs: detect - if: ${{ needs.detect.outputs.eligible == 'true' }} - outputs: - image: ${{ steps.meta.outputs.image }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.detect.outputs.ref }} - - - uses: docker/setup-buildx-action@v3 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - id: meta - run: | - owner="${{ github.repository_owner }}" - owner_lc="${owner,,}" - revision="$(git rev-parse HEAD)" - image="ghcr.io/${owner_lc}/household-bot-bot:${revision}" - echo "image=$image" >> "$GITHUB_OUTPUT" - - - uses: docker/build-push-action@v6 - with: - context: . - file: apps/bot/Dockerfile - push: true - tags: | - ${{ steps.meta.outputs.image }} - platforms: linux/amd64 - provenance: false - - build-miniapp: - runs-on: ubuntu-latest - needs: detect - if: ${{ needs.detect.outputs.eligible == 'true' }} - outputs: - image: ${{ steps.meta.outputs.image }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.detect.outputs.ref }} - - - uses: docker/setup-buildx-action@v3 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - id: meta - run: | - owner="${{ github.repository_owner }}" - owner_lc="${owner,,}" - revision="$(git rev-parse HEAD)" - image="ghcr.io/${owner_lc}/household-bot-miniapp:${revision}" - echo "image=$image" >> "$GITHUB_OUTPUT" - - - uses: docker/build-push-action@v6 - with: - context: . - file: apps/miniapp/Dockerfile - push: true - tags: | - ${{ steps.meta.outputs.image }} - platforms: linux/amd64 - provenance: false - - deploy: - runs-on: ubuntu-latest - needs: [detect, build-bot, build-miniapp] - if: ${{ needs.detect.outputs.eligible == 'true' }} - env: - VPS_HOST: ${{ vars.VPS_HOST }} - VPS_USER: ${{ vars.VPS_USER || 'root' }} - VPS_PORT: ${{ vars.VPS_PORT || '22' }} - DEPLOY_ROOT: ${{ vars.VPS_DEPLOY_ROOT || '/opt/household-bot' }} - BOT_IMAGE: ${{ needs.build-bot.outputs.image }} - MINIAPP_IMAGE: ${{ needs.build-miniapp.outputs.image }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.detect.outputs.ref }} - - - name: Validate deploy config - run: | - test -n "$VPS_HOST" - test -n "$BOT_IMAGE" - test -n "$MINIAPP_IMAGE" - test -n "${{ secrets.VPS_SSH_KEY }}" - - - name: Prepare SSH - env: - VPS_KNOWN_HOSTS: ${{ secrets.VPS_KNOWN_HOSTS }} - run: | - install -m 700 -d ~/.ssh - printf '%s\n' '${{ secrets.VPS_SSH_KEY }}' > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - test -n "$VPS_KNOWN_HOSTS" - printf '%s\n' "$VPS_KNOWN_HOSTS" > ~/.ssh/known_hosts - - - name: Upload deploy bundle - run: | - ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "mkdir -p '$DEPLOY_ROOT/app'" - tar czf - deploy/vps scripts/ops/vps-deploy.sh | \ - ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "tar xzf - -C '$DEPLOY_ROOT/app'" - - - name: Run remote deploy - run: | - ssh -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" \ - "export DEPLOY_ROOT='$DEPLOY_ROOT' APP_DIR='$DEPLOY_ROOT/app' ENV_DIR='$DEPLOY_ROOT/env' BOT_IMAGE='$BOT_IMAGE' MINIAPP_IMAGE='$MINIAPP_IMAGE' GHCR_USERNAME='${{ github.actor }}' GHCR_TOKEN='${{ secrets.GITHUB_TOKEN }}'; '$DEPLOY_ROOT/app/scripts/ops/vps-deploy.sh'" - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: .bun-version - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Set Telegram webhook - env: - TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TELEGRAM_WEBHOOK_SECRET: ${{ secrets.TELEGRAM_WEBHOOK_SECRET }} - TELEGRAM_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram - run: | - if [[ -z "${TELEGRAM_BOT_TOKEN:-}" || -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]]; then - echo "Skipping webhook sync: TELEGRAM_BOT_TOKEN or TELEGRAM_WEBHOOK_SECRET is missing" - exit 0 - fi - - bun run ops:telegram:webhook set - - - name: Smoke check - env: - BOT_API_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }} - MINI_APP_URL: ${{ vars.VPS_MINIAPP_URL || 'https://household.whekin.dev' }} - TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} - TELEGRAM_EXPECTED_WEBHOOK_URL: ${{ vars.VPS_BOT_URL || 'https://household-bot.whekin.dev' }}/webhook/telegram - run: bun run ops:deploy:smoke diff --git a/deploy/coolify/compose.yml b/deploy/coolify/compose.yml new file mode 100644 index 0000000..753da80 --- /dev/null +++ b/deploy/coolify/compose.yml @@ -0,0 +1,102 @@ +services: + bot: + build: + context: ../.. + dockerfile: apps/bot/Dockerfile + environment: + NODE_ENV: production + PORT: ${BOT_PORT:?8080} + LOG_LEVEL: ${LOG_LEVEL:-info} + DATABASE_URL: ${DATABASE_URL:?} + DB_SCHEMA: ${DB_SCHEMA:-public} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:?} + TELEGRAM_WEBHOOK_SECRET: ${TELEGRAM_WEBHOOK_SECRET:?} + TELEGRAM_WEBHOOK_PATH: ${TELEGRAM_WEBHOOK_PATH:-/webhook/telegram} + MINI_APP_URL: ${MINI_APP_URL:?} + MINI_APP_ALLOWED_ORIGINS: ${MINI_APP_ALLOWED_ORIGINS:?} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + PURCHASE_PARSER_MODEL: ${PURCHASE_PARSER_MODEL:-gpt-4o-mini} + ASSISTANT_MODEL: ${ASSISTANT_MODEL:-gpt-4o-mini} + TOPIC_PROCESSOR_MODEL: ${TOPIC_PROCESSOR_MODEL:-gpt-4o-mini} + TOPIC_PROCESSOR_TIMEOUT_MS: ${TOPIC_PROCESSOR_TIMEOUT_MS:-10000} + ASSISTANT_TIMEOUT_MS: ${ASSISTANT_TIMEOUT_MS:-20000} + ASSISTANT_MEMORY_MAX_TURNS: ${ASSISTANT_MEMORY_MAX_TURNS:-12} + ASSISTANT_RATE_LIMIT_BURST: ${ASSISTANT_RATE_LIMIT_BURST:-5} + ASSISTANT_RATE_LIMIT_BURST_WINDOW_MS: ${ASSISTANT_RATE_LIMIT_BURST_WINDOW_MS:-60000} + ASSISTANT_RATE_LIMIT_ROLLING: ${ASSISTANT_RATE_LIMIT_ROLLING:-50} + ASSISTANT_RATE_LIMIT_ROLLING_WINDOW_MS: ${ASSISTANT_RATE_LIMIT_ROLLING_WINDOW_MS:-86400000} + SCHEDULER_SHARED_SECRET: ${SCHEDULER_SHARED_SECRET:?} + SCHEDULED_DISPATCH_PROVIDER: ${SCHEDULED_DISPATCH_PROVIDER:?self-hosted} + SCHEDULER_OIDC_ALLOWED_EMAILS: ${SCHEDULER_OIDC_ALLOWED_EMAILS:-} + SCHEDULED_DISPATCH_PUBLIC_BASE_URL: ${SCHEDULED_DISPATCH_PUBLIC_BASE_URL:-} + GCP_SCHEDULED_DISPATCH_PROJECT_ID: ${GCP_SCHEDULED_DISPATCH_PROJECT_ID:-} + GCP_SCHEDULED_DISPATCH_LOCATION: ${GCP_SCHEDULED_DISPATCH_LOCATION:-} + GCP_SCHEDULED_DISPATCH_QUEUE: ${GCP_SCHEDULED_DISPATCH_QUEUE:-} + AWS_SCHEDULED_DISPATCH_REGION: ${AWS_SCHEDULED_DISPATCH_REGION:-} + AWS_SCHEDULED_DISPATCH_TARGET_LAMBDA_ARN: ${AWS_SCHEDULED_DISPATCH_TARGET_LAMBDA_ARN:-} + AWS_SCHEDULED_DISPATCH_ROLE_ARN: ${AWS_SCHEDULED_DISPATCH_ROLE_ARN:-} + AWS_SCHEDULED_DISPATCH_GROUP_NAME: ${AWS_SCHEDULED_DISPATCH_GROUP_NAME:-} + command: + - /bin/sh + - -lc + - bun packages/db/dist/migrate.js && exec bun apps/bot/dist/index.js + healthcheck: + test: + - CMD + - bun + - -e + - "fetch('http://127.0.0.1:' + (process.env.PORT ?? '8080') + '/healthz').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))" + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + restart: unless-stopped + + miniapp: + build: + context: ../.. + dockerfile: apps/miniapp/Dockerfile + environment: + BOT_API_URL: ${BOT_API_URL:?} + depends_on: + bot: + condition: service_healthy + restart: unless-stopped + + scheduler: + build: + context: ../.. + dockerfile: apps/bot/Dockerfile + command: + - bun + - apps/bot/dist/scheduler-runner.js + environment: + NODE_ENV: production + LOG_LEVEL: ${LOG_LEVEL:-info} + DATABASE_URL: ${DATABASE_URL:?} + DB_SCHEMA: ${DB_SCHEMA:-public} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:?} + TELEGRAM_WEBHOOK_SECRET: ${TELEGRAM_WEBHOOK_SECRET:?} + TELEGRAM_WEBHOOK_PATH: ${TELEGRAM_WEBHOOK_PATH:-/webhook/telegram} + MINI_APP_URL: ${MINI_APP_URL:?} + MINI_APP_ALLOWED_ORIGINS: ${MINI_APP_ALLOWED_ORIGINS:?} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + PURCHASE_PARSER_MODEL: ${PURCHASE_PARSER_MODEL:-gpt-4o-mini} + ASSISTANT_MODEL: ${ASSISTANT_MODEL:-gpt-4o-mini} + TOPIC_PROCESSOR_MODEL: ${TOPIC_PROCESSOR_MODEL:-gpt-4o-mini} + TOPIC_PROCESSOR_TIMEOUT_MS: ${TOPIC_PROCESSOR_TIMEOUT_MS:-10000} + ASSISTANT_TIMEOUT_MS: ${ASSISTANT_TIMEOUT_MS:-20000} + ASSISTANT_MEMORY_MAX_TURNS: ${ASSISTANT_MEMORY_MAX_TURNS:-12} + ASSISTANT_RATE_LIMIT_BURST: ${ASSISTANT_RATE_LIMIT_BURST:-5} + ASSISTANT_RATE_LIMIT_BURST_WINDOW_MS: ${ASSISTANT_RATE_LIMIT_BURST_WINDOW_MS:-60000} + ASSISTANT_RATE_LIMIT_ROLLING: ${ASSISTANT_RATE_LIMIT_ROLLING:-50} + ASSISTANT_RATE_LIMIT_ROLLING_WINDOW_MS: ${ASSISTANT_RATE_LIMIT_ROLLING_WINDOW_MS:-86400000} + SCHEDULER_SHARED_SECRET: ${SCHEDULER_SHARED_SECRET:?} + SCHEDULED_DISPATCH_PROVIDER: ${SCHEDULED_DISPATCH_PROVIDER:?self-hosted} + BOT_INTERNAL_BASE_URL: http://bot:8080 + SCHEDULER_POLL_INTERVAL_MS: ${SCHEDULER_POLL_INTERVAL_MS:-60000} + SCHEDULER_DUE_SCAN_LIMIT: ${SCHEDULER_DUE_SCAN_LIMIT:-25} + depends_on: + bot: + condition: service_healthy + restart: unless-stopped diff --git a/deploy/vps/Caddyfile b/deploy/vps/Caddyfile deleted file mode 100644 index bf06b3c..0000000 --- a/deploy/vps/Caddyfile +++ /dev/null @@ -1,9 +0,0 @@ -{$BOT_DOMAIN} { - encode zstd gzip - reverse_proxy bot:8080 -} - -{$MINIAPP_DOMAIN} { - encode zstd gzip - reverse_proxy miniapp:8080 -} diff --git a/deploy/vps/bot.env.example b/deploy/vps/bot.env.example deleted file mode 100644 index bc4a45e..0000000 --- a/deploy/vps/bot.env.example +++ /dev/null @@ -1,39 +0,0 @@ -NODE_ENV=production -LOG_LEVEL=info -PORT=8080 - -DATABASE_URL=postgres://... -DB_SCHEMA=public - -TELEGRAM_BOT_TOKEN=... -TELEGRAM_WEBHOOK_SECRET=... -TELEGRAM_WEBHOOK_PATH=/webhook/telegram - -MINI_APP_URL=https://household.whekin.dev -MINI_APP_ALLOWED_ORIGINS=https://household.whekin.dev - -OPENAI_API_KEY= -PURCHASE_PARSER_MODEL=gpt-4o-mini -ASSISTANT_MODEL=gpt-4o-mini -TOPIC_PROCESSOR_MODEL=gpt-4o-mini -TOPIC_PROCESSOR_TIMEOUT_MS=10000 -ASSISTANT_TIMEOUT_MS=20000 -ASSISTANT_MEMORY_MAX_TURNS=12 -ASSISTANT_RATE_LIMIT_BURST=5 -ASSISTANT_RATE_LIMIT_BURST_WINDOW_MS=60000 -ASSISTANT_RATE_LIMIT_ROLLING=50 -ASSISTANT_RATE_LIMIT_ROLLING_WINDOW_MS=86400000 - -SCHEDULER_SHARED_SECRET=replace-with-random-secret -SCHEDULED_DISPATCH_PROVIDER=self-hosted - -# Keep for cloud compatibility / fallback. -SCHEDULER_OIDC_ALLOWED_EMAILS= -SCHEDULED_DISPATCH_PUBLIC_BASE_URL= -GCP_SCHEDULED_DISPATCH_PROJECT_ID= -GCP_SCHEDULED_DISPATCH_LOCATION= -GCP_SCHEDULED_DISPATCH_QUEUE= -AWS_SCHEDULED_DISPATCH_REGION= -AWS_SCHEDULED_DISPATCH_TARGET_LAMBDA_ARN= -AWS_SCHEDULED_DISPATCH_ROLE_ARN= -AWS_SCHEDULED_DISPATCH_GROUP_NAME= diff --git a/deploy/vps/caddy.env.example b/deploy/vps/caddy.env.example deleted file mode 100644 index afb11ee..0000000 --- a/deploy/vps/caddy.env.example +++ /dev/null @@ -1,2 +0,0 @@ -BOT_DOMAIN=household-bot.whekin.dev -MINIAPP_DOMAIN=household.whekin.dev diff --git a/deploy/vps/compose.yml b/deploy/vps/compose.yml deleted file mode 100644 index 5d4b96e..0000000 --- a/deploy/vps/compose.yml +++ /dev/null @@ -1,74 +0,0 @@ -services: - bot: - image: ${BOT_IMAGE:?set BOT_IMAGE} - restart: unless-stopped - env_file: - - ${ENV_DIR:-/opt/household-bot/env}/bot.env - healthcheck: - test: - - CMD - - bun - - -e - - "fetch('http://127.0.0.1:' + (process.env.PORT ?? '8080') + '/healthz').then((res) => process.exit(res.ok ? 0 : 1)).catch(() => process.exit(1))" - interval: 30s - timeout: 5s - retries: 3 - start_period: 15s - - miniapp: - image: ${MINIAPP_IMAGE:?set MINIAPP_IMAGE} - restart: unless-stopped - env_file: - - ${ENV_DIR:-/opt/household-bot/env}/miniapp.env - depends_on: - bot: - condition: service_healthy - - scheduler: - image: ${BOT_IMAGE:?set BOT_IMAGE} - restart: unless-stopped - command: - - bun - - apps/bot/dist/scheduler-runner.js - env_file: - - ${ENV_DIR:-/opt/household-bot/env}/bot.env - environment: - BOT_INTERNAL_BASE_URL: http://bot:8080 - SCHEDULER_POLL_INTERVAL_MS: ${SCHEDULER_POLL_INTERVAL_MS:-60000} - SCHEDULER_DUE_SCAN_LIMIT: ${SCHEDULER_DUE_SCAN_LIMIT:-25} - depends_on: - bot: - condition: service_healthy - - migrate: - image: ${BOT_IMAGE:?set BOT_IMAGE} - profiles: - - ops - env_file: - - ${ENV_DIR:-/opt/household-bot/env}/bot.env - command: - - bun - - packages/db/dist/migrate.js - restart: 'no' - - caddy: - image: caddy:2.8-alpine - restart: unless-stopped - env_file: - - ${ENV_DIR:-/opt/household-bot/env}/caddy.env - depends_on: - bot: - condition: service_healthy - miniapp: - condition: service_started - ports: - - '80:80' - - '443:443' - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - -volumes: - caddy_data: - caddy_config: diff --git a/deploy/vps/miniapp.env.example b/deploy/vps/miniapp.env.example deleted file mode 100644 index 290a7ca..0000000 --- a/deploy/vps/miniapp.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_BOT_API_URL=https://household-bot.whekin.dev diff --git a/docs/runbooks/coolify-compose-deploy.md b/docs/runbooks/coolify-compose-deploy.md new file mode 100644 index 0000000..822dd75 --- /dev/null +++ b/docs/runbooks/coolify-compose-deploy.md @@ -0,0 +1,126 @@ +# Coolify Docker Compose Deployment Plan + +## Goal + +Deploy `household-bot` on a VPS that already runs Coolify, while keeping Supabase external and preserving cloud compatibility in the codebase. + +## Why Coolify-first + +Coolify already provides: + +- Git-based deployments +- domains and TLS +- environment variable management +- Docker Compose deployment support + +That makes it a better target than a hand-rolled VPS deploy workflow for this project. + +## Deployment shape + +Use a Docker Compose deployment in Coolify with three services: + +- `bot` — Telegram webhook/API +- `miniapp` — static frontend container +- `scheduler` — periodic due-dispatch runner + +Database stays external: + +- Supabase / managed Postgres + +## Compose principles for Coolify + +For Coolify Compose deployments: + +- the compose file is the source of truth +- define environment variables inline with `${VAR}` placeholders +- let Coolify manage domains/proxying instead of bundling Caddy/Nginx reverse proxy for the public edge +- do not rely on external `env_file` paths on the host + +## Scheduler strategy + +Keep the self-hosted scheduled dispatch provider introduced in this PR. + +Runtime model: + +- `bot` handles webhook/API traffic +- `scheduler` calls the internal due-dispatch endpoint repeatedly +- both services share the same app image build but run different commands + +## Migrations + +For the first Coolify version, run DB migrations from the bot startup command before the server starts. + +This is intentionally pragmatic: + +- no extra one-off deploy script is required +- no host SSH deploy step is required +- drizzle migrations are idempotent enough for single-service startup usage here + +If the deployment setup matures later, split migrations into a dedicated release/predeploy step. + +## Domains + +Suggested public domains: + +- `household-bot.whekin.dev` -> `bot` +- `household.whekin.dev` -> `miniapp` + +Coolify should manage the public routing/TLS for these services. + +## Required Coolify variables + +Core bot/runtime: + +- `DATABASE_URL` +- `DB_SCHEMA` (default `public`) +- `TELEGRAM_BOT_TOKEN` +- `TELEGRAM_WEBHOOK_SECRET` +- `TELEGRAM_WEBHOOK_PATH` +- `MINI_APP_URL` +- `MINI_APP_ALLOWED_ORIGINS` +- `SCHEDULER_SHARED_SECRET` +- `SCHEDULED_DISPATCH_PROVIDER` (`self-hosted`) + +Optional AI/runtime: + +- `OPENAI_API_KEY` +- `PURCHASE_PARSER_MODEL` +- `ASSISTANT_MODEL` +- `TOPIC_PROCESSOR_MODEL` +- assistant timeout/rate-limit variables + +Miniapp: + +- `BOT_API_URL` + +Scheduler: + +- `SCHEDULER_POLL_INTERVAL_MS` +- `SCHEDULER_DUE_SCAN_LIMIT` + +## Cloud compatibility rule + +Keep these intact in the app/config layer even if Coolify becomes the main path: + +- Cloud Run compatibility +- AWS compatibility +- existing cloud-specific scheduler env vars/adapters + +The deployment target changes; the app should not become Coolify-only. + +## Recommended rollout + +1. Add Coolify compose file +2. Remove VPS-specific deploy glue from this PR +3. Create a Coolify Docker Compose app from the repo +4. Fill required variables in Coolify UI +5. Assign domains to `bot` and `miniapp` +6. Deploy and verify webhook + miniapp + scheduler behavior + +## Notes for later + +Possible future upgrades: + +- add Codex/CodeRabbit review automation around PRs +- move migrations to a dedicated release step +- codify Coolify resources with Terraform/Pulumi later if that still feels worth it diff --git a/docs/runbooks/vps-compose-deploy.md b/docs/runbooks/vps-compose-deploy.md deleted file mode 100644 index 6ecaab8..0000000 --- a/docs/runbooks/vps-compose-deploy.md +++ /dev/null @@ -1,182 +0,0 @@ -# VPS Docker Compose Deployment Plan - -## Goal - -Make the VPS deployment path first-class without removing the existing Cloud Run / AWS paths. - -Primary target: - -- bot API on Docker Compose -- mini app on Docker Compose -- reverse proxy with HTTPS on the VPS -- scheduled reminder delivery without Cloud Tasks / Cloud Scheduler -- GitHub Actions CD that deploys to the VPS - -Compatibility requirement: - -- keep existing cloud deployment code and workflows available -- avoid deleting GCP/AWS-specific adapters unless they are clearly dead and isolated - -## Deployment Shape - -Recommended production services: - -- `bot` - Bun runtime for Telegram webhook/API -- `miniapp` - static assets served behind reverse proxy -- `scheduler` - separate service that periodically triggers due scheduled dispatch processing -- `caddy` - TLS + reverse proxy for `bot.` and `app.` - -Database: - -- keep Supabase / managed Postgres external -- do not move Postgres onto the VPS in this phase - -## Scheduler Replacement Strategy - -Current app logic already stores scheduled dispatches in Postgres and uses provider adapters for one-shot execution. - -For VPS: - -1. Add a self-hosted scheduled dispatch provider. -2. Keep dispatch records in the database as before. -3. Add a due-dispatch scan endpoint/handler in the bot runtime. -4. Run a dedicated scheduler service in Compose that periodically triggers that scan. -5. Keep GCP Cloud Tasks and AWS EventBridge adapters intact for backward compatibility. - -This keeps reminder behavior deterministic while removing dependency on cloud schedulers. - -## Image / Runtime Plan - -### Bot image - -- keep multi-stage build -- build runtime entrypoints for: - - bot server - - scheduler runner - - DB migrate command -- keep runtime image lean - -### Mini app image - -- keep static build + nginx/alpine runtime - -### Reverse proxy image - -- use an off-the-shelf slim image (Caddy) - -## CD Plan - -Add a separate GitHub Actions workflow for VPS deploy: - -1. run on successful `main` CI and manual dispatch -2. build/push bot and miniapp images to GHCR -3. SSH into VPS -4. pull latest images -5. run DB migrations -6. restart Compose services -7. run smoke checks -8. sync Telegram webhook - -Keep existing GCP and AWS workflows untouched. - -## Secrets / Env Plan - -Phase 1: - -- keep runtime env files on the VPS outside the repo -- Compose loads env files from a deploy directory - -Optional later upgrade: - -- add 1Password-backed rendering or injection without changing app runtime contracts -- keep the runtime contract env-file-based so 1Password remains an overlay, not a hard dependency - -Compatibility rule: - -- do not remove existing env vars for GCP/AWS paths -- only add new VPS/self-hosted vars where needed - -## Expected Repo Changes - -### App/runtime - -- add self-hosted scheduler adapter -- add due-dispatch scan support -- add scheduler runner entrypoint -- extend config parsing with VPS/self-hosted provider - -### Docker / deploy - -- add production compose file -- add Caddy config -- add VPS deploy helper scripts - -### CI/CD - -- add VPS deploy workflow -- keep `cd.yml` and `cd-aws.yml` - -### Docs - -- add VPS deployment runbook -- document required env files and domains - -## Domain Assumption - -Base domain: `whekin.dev` - -Suggested hostnames: - -- `household-bot.whekin.dev` for bot API / webhook -- `household.whekin.dev` for mini app - -These can be adjusted later without changing the deployment shape. - -## Rollout Order - -1. Add docs/plan -2. Implement self-hosted scheduler path -3. Add production compose + reverse proxy config -4. Add VPS deploy scripts -5. Add GitHub Actions VPS CD -6. Validate builds/tests where practical -7. Push branch and open PR - -## Runtime Env Files on VPS - -Expected files under `/opt/household-bot/env`: - -- `bot.env` -- `miniapp.env` -- `caddy.env` - -Templates live in `deploy/vps/*.env.example`. - -- `miniapp.env` should set `VITE_BOT_API_URL` for the frontend build/runtime config. - -## GitHub Actions Inputs / Secrets - -Recommended repository variables: - -- `VPS_HOST` -- `VPS_USER` (default `root`) -- `VPS_PORT` (default `22`) -- `VPS_DEPLOY_ROOT` (default `/opt/household-bot`) -- `VPS_BOT_URL` (default `https://household-bot.whekin.dev`) -- `VPS_MINIAPP_URL` (default `https://household.whekin.dev`) - -Required repository secrets: - -- `VPS_SSH_KEY` -- `VPS_KNOWN_HOSTS` - -Optional for webhook sync and smoke verification: - -- `TELEGRAM_BOT_TOKEN` -- `TELEGRAM_WEBHOOK_SECRET` - -## First-Time VPS Bootstrap - -Use `scripts/ops/vps-bootstrap-ubuntu.sh` to install Docker Engine + Compose plugin on Ubuntu 24.04. - -Then place env files in `/opt/household-bot/env` before running the first deploy. diff --git a/scripts/ops/vps-bootstrap-ubuntu.sh b/scripts/ops/vps-bootstrap-ubuntu.sh deleted file mode 100755 index 2d3ff76..0000000 --- a/scripts/ops/vps-bootstrap-ubuntu.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$(id -u)" -ne 0 ]]; then - echo "Run as root" >&2 - exit 1 -fi - -apt-get update -apt-get install -y ca-certificates curl gnupg -install -m 0755 -d /etc/apt/keyrings -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg -chmod a+r /etc/apt/keyrings/docker.gpg - -echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ - | tee /etc/apt/sources.list.d/docker.list >/dev/null - -apt-get update -apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -systemctl enable docker -systemctl start docker - -mkdir -p /opt/household-bot/app /opt/household-bot/env - -echo "Docker installed. Next: place env files in /opt/household-bot/env and deploy the app bundle to /opt/household-bot/app" diff --git a/scripts/ops/vps-deploy.sh b/scripts/ops/vps-deploy.sh deleted file mode 100755 index b0c21d0..0000000 --- a/scripts/ops/vps-deploy.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/household-bot}" -APP_DIR="${APP_DIR:-$DEPLOY_ROOT/app}" -ENV_DIR="${ENV_DIR:-$DEPLOY_ROOT/env}" -COMPOSE_FILE="${COMPOSE_FILE:-$APP_DIR/deploy/vps/compose.yml}" - -require_file() { - local path="$1" - if [[ ! -f "$path" ]]; then - echo "Missing required file: $path" >&2 - exit 1 - fi -} - -require_var() { - local name="$1" - if [[ -z "${!name:-}" ]]; then - echo "Missing required variable: $name" >&2 - exit 1 - fi -} - -require_positive_int() { - local name="$1" - local value="${!name:-}" - if [[ ! "$value" =~ ^[0-9]+$ || "$value" -le 0 ]]; then - echo "Invalid positive integer for $name: $value" >&2 - exit 1 - fi -} - -require_var BOT_IMAGE -require_var MINIAPP_IMAGE -require_file "$COMPOSE_FILE" -require_file "$ENV_DIR/bot.env" -require_file "$ENV_DIR/miniapp.env" -require_file "$ENV_DIR/caddy.env" - -if [[ -n "${GHCR_USERNAME:-}" && -n "${GHCR_TOKEN:-}" ]]; then - echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin -fi - -export BOT_IMAGE -export MINIAPP_IMAGE -export ENV_DIR -export SCHEDULER_POLL_INTERVAL_MS="${SCHEDULER_POLL_INTERVAL_MS:-60000}" -export SCHEDULER_DUE_SCAN_LIMIT="${SCHEDULER_DUE_SCAN_LIMIT:-25}" -require_positive_int SCHEDULER_POLL_INTERVAL_MS -require_positive_int SCHEDULER_DUE_SCAN_LIMIT - -mkdir -p "$DEPLOY_ROOT" - -docker compose -f "$COMPOSE_FILE" pull bot miniapp scheduler migrate caddy - -docker compose -f "$COMPOSE_FILE" run --rm --no-deps migrate - -docker compose -f "$COMPOSE_FILE" up -d --remove-orphans bot miniapp scheduler caddy - -docker compose -f "$COMPOSE_FILE" ps