diff --git a/docs/deployment/hetzner-server-setup.md b/docs/deployment/hetzner-server-setup.md index 9eb82011..c7072b3a 100644 --- a/docs/deployment/hetzner-server-setup.md +++ b/docs/deployment/hetzner-server-setup.md @@ -559,7 +559,7 @@ docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" ``` !!! note "After the reset" - `init_production.py` re-creates the four admin users with their **default** passwords (see `init_production.py:280-300`). Any admin-side configuration that lives in the `admin_settings` table (e.g. the manual SMTP overrides under `/admin/settings`) is wiped and must be re-applied. The `/health` endpoint reads `.build-info` which is only regenerated by `scripts/deploy.sh`, so after a manual reset it will report the **previous** commit; harmless but worth knowing. + `init_production.py` re-creates the four admin users with their **default** passwords (see `init_production.py:280-300`). Any admin-side configuration that lives in the `admin_settings` table (e.g. the manual SMTP overrides under `/admin/settings`) is wiped and must be re-applied. The `/health` endpoint reads `.build-info` which is only regenerated by `scripts/deploy.sh` or `scripts/deploy-api-only.sh` (see [Step 16.5](#165-manual-deploy)), so after a manual reset it will report the **previous** commit; harmless but worth knowing. ### Seeded Data Summary @@ -1197,14 +1197,51 @@ deploy: script: cd ${{ secrets.DEPLOY_PATH }} && bash scripts/deploy.sh ``` -### 16.5 Manual Fallback +### 16.5 Manual Deploy -If CI is down, deploy manually: +Two manual paths, pick the right one for the change you're shipping. + +#### 16.5a — Code-only fix (default for ad-hoc manual deploys) + +For frontend / template / api-only changes that don't touch the Dockerfile, +requirements.txt, docker-compose.yml, or alembic migrations. Rebuilds and +restarts **only** the api container — db, redis, celery-worker, celery-beat, +flower stay running. + +```bash +cd ~/apps/orion && bash scripts/deploy-api-only.sh +``` + +What it does (`scripts/deploy-api-only.sh`): + +1. Refuses if working tree is dirty (no silent stash → no risk of pop conflicts). +2. `git pull --ff-only`. +3. **Writes `.build-info`** — this is the critical step that ensures the + `?v=` cache-bust query on every shared JS/CSS URL flips to the + new SHA. Without this, browsers happily keep serving the previous + deploy's cached assets even though the new code is in the image. +4. `docker compose -f docker-compose.yml --profile full up -d --build api`. +5. Health-check with a 30s budget (tight, since the DB/Redis weren't touched). + +Exit codes: `0` success, `1` git pull / dirty tree, `2` docker build/up +failed, `3` health check failed. + +#### 16.5b — Full deploy (use when CI is down) + +Use this when you've also changed migrations, the Dockerfile, +requirements.txt, or docker-compose.yml itself — anything that needs the +full restart-everything + migrate cycle that the CI runs. Restarts EVERY +service in the `full` profile (db, redis, api, celery-worker, celery-beat, +flower) and runs `alembic upgrade heads`. ```bash cd ~/apps/orion && bash scripts/deploy.sh ``` +Heavier — brief DB downtime, Redis is blown away (sessions / rate-limit +counters / cached anything), in-flight Celery tasks killed — so don't use +it for code-only fixes. + ### 16.6 Verify ```bash diff --git a/scripts/deploy-api-only.sh b/scripts/deploy-api-only.sh new file mode 100755 index 00000000..6e1e306d --- /dev/null +++ b/scripts/deploy-api-only.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# ============================================================================= +# Orion Manual API-Only Redeploy +# ============================================================================= +# Usage: cd ~/apps/orion && bash scripts/deploy-api-only.sh +# +# Narrow, code-only redeploy of the api container for ad-hoc fixes. Compared +# to the full scripts/deploy.sh (used by Gitea CI/CD), this script: +# - Does NOT stash/pop local changes (fails loudly if working tree is dirty +# so you can decide what to do, instead of silently rolling them through). +# - Rebuilds and restarts ONLY the api service — leaves db, redis, +# celery-worker, celery-beat, flower untouched (no Redis flush, no +# in-flight Celery task kills, no DB downtime). +# - Skips alembic upgrade (manual redeploys are usually code-only). +# - Health-checks with a tighter 30s budget since the DB doesn't need to +# come back up. +# +# Use this when shipping a frontend/template fix or other api-only change. +# Use bash scripts/deploy.sh instead when running migrations, changing +# Dockerfile / requirements.txt / docker-compose.yml, or hitting any +# infrastructure config. +# +# Exit codes: +# 0 — success +# 1 — git pull failed (non-fast-forward, or dirty working tree blocking pull) +# 2 — docker compose build/up failed +# 3 — health check failed +# ============================================================================= + +set -euo pipefail + +COMPOSE="docker compose -f docker-compose.yml --profile full" +HEALTH_URL="http://localhost:8001/health" +HEALTH_RETRIES=6 +HEALTH_INTERVAL=5 + +log() { echo "[deploy-api] $(date '+%H:%M:%S') $*"; } + +# ── 1. Refuse if working tree is dirty ────────────────────────────────────── +# Unlike deploy.sh we do not stash, because stash+pop has a history of +# producing conflicts when prod has hand-edited config files. Surface the +# state so you can deal with it manually. +if [ -n "$(git status --porcelain)" ]; then + log "ERROR: working tree has local changes — refusing to deploy." + log " Commit, stash, or revert them yourself, then re-run." + git status --short + exit 1 +fi + +# ── 2. Pull latest code (fast-forward only) ───────────────────────────────── +log "Pulling latest code …" +if ! git pull --ff-only; then + log "ERROR: git pull failed (non-fast-forward?). Resolve and re-run." + exit 1 +fi + +# ── 3. Write build info ───────────────────────────────────────────────────── +# This file is bind-mounted into the api container and read by +# app/core/build_info.py → templates_config._asset_version(). It drives +# the ?v= cache-bust on every shared JS/CSS URL. If it stays stale, +# browsers keep cached pre-fix assets even after a successful rebuild. +log "Writing build info …" +printf '{"commit":"%s","deployed_at":"%s"}\n' \ + "$(git rev-parse --short=8 HEAD)" \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + > .build-info + +# ── 4. Rebuild + recreate api ONLY ─────────────────────────────────────────── +log "Rebuilding api container …" +if ! $COMPOSE up -d --build api; then + log "ERROR: docker compose up --build api failed" + exit 2 +fi + +# ── 5. Health check (tight budget — DB/Redis weren't touched) ─────────────── +log "Waiting for health check ($HEALTH_URL) …" +for i in $(seq 1 "$HEALTH_RETRIES"); do + if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then + log "Health check passed (attempt $i/$HEALTH_RETRIES)" + log "Deploy complete. ?v=$(git rev-parse --short=8 HEAD) now live." + exit 0 + fi + log "Health check attempt $i/$HEALTH_RETRIES failed, retrying in ${HEALTH_INTERVAL}s …" + sleep "$HEALTH_INTERVAL" +done + +log "ERROR: health check failed after $HEALTH_RETRIES attempts" +log " Container may still be starting — check 'docker compose --profile full ps'" +log " and 'docker compose --profile full logs api --tail=50'." +exit 3