refactor(deploy): pivot compose target to coolify

Co-authored-by: claw <stanislavkalishin+claw@gmail.com>
This commit is contained in:
2026-03-30 22:52:14 +02:00
parent 160d922b8b
commit 116e403f7a
11 changed files with 228 additions and 589 deletions

View File

@@ -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

102
deploy/coolify/compose.yml Normal file
View File

@@ -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

View File

@@ -1,9 +0,0 @@
{$BOT_DOMAIN} {
encode zstd gzip
reverse_proxy bot:8080
}
{$MINIAPP_DOMAIN} {
encode zstd gzip
reverse_proxy miniapp:8080
}

View File

@@ -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=

View File

@@ -1,2 +0,0 @@
BOT_DOMAIN=household-bot.whekin.dev
MINIAPP_DOMAIN=household.whekin.dev

View File

@@ -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:

View File

@@ -1 +0,0 @@
VITE_BOT_API_URL=https://household-bot.whekin.dev

View File

@@ -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

View File

@@ -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.<domain>` and `app.<domain>`
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.

View File

@@ -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"

View File

@@ -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