From 5fbc175e540803d919863f3d90dffc3c0645a90b Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sat, 30 May 2026 00:21:03 -0700 Subject: feat: add scripts/deploy.sh for idempotent production deploys Handles first-time install (--install) and redeploys: pulls, builds backend and frontend as www-data with the required HOME/NPM_CONFIG_CACHE env, restarts systemd services, and smoke-checks /health and /. Co-Authored-By: Claude Opus 4.7 --- AGENTS.md | 9 ++-- CLAUDE.md | 9 +--- README.md | 54 ++++++++---------------- scripts/deploy.sh | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 51 deletions(-) create mode 100755 scripts/deploy.sh diff --git a/AGENTS.md b/AGENTS.md index ac36c80..fbb2f2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,14 +29,11 @@ cd frontend && npm run lint && npm run build Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy (`/api/` → backend, `/` → frontend). See `README.md` for the full deploy and redeploy commands. Service user is `www-data`; code lives at `/var/www/prism-v2/`. -Redeploy in one shot: +Redeploy: ```bash -cd /var/www/prism-v2 && sudo -u www-data git pull origin master -sudo -u www-data backend/.venv/bin/pip install -r backend/requirements.txt -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm npm --prefix frontend ci -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm npm --prefix frontend run build -sudo systemctl restart prismv2-backend.service prismv2-frontend.service +cd /var/www/prism-v2 && sudo ./scripts/deploy.sh +# --no-pull to skip git pull; --install to also refresh systemd units + nginx site ``` ## Coding Style diff --git a/CLAUDE.md b/CLAUDE.md index 8ac44a0..5fc6417 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,14 +41,7 @@ Key files: Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy at `prism.tylerhoang.xyz` (`/api/` → backend, `/` → frontend). Service user `www-data`, code at `/var/www/prism-v2/`. Full deploy instructions in `README.md`. -**Redeploy:** -```bash -cd /var/www/prism-v2 && sudo -u www-data git pull origin master -sudo -u www-data backend/.venv/bin/pip install -r backend/requirements.txt -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm npm --prefix frontend ci -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm npm --prefix frontend run build -sudo systemctl restart prismv2-backend.service prismv2-frontend.service -``` +**Redeploy:** `sudo ./scripts/deploy.sh` from `/var/www/prism-v2` (idempotent — pull, install deps as `www-data`, build, restart, smoke-check). Flags: `--no-pull`, `--install` (refresh systemd units + nginx site). **Logs / status:** ```bash diff --git a/README.md b/README.md index 9e7f689..9f6b77d 100644 --- a/README.md +++ b/README.md @@ -53,55 +53,35 @@ SQLite lives at `backend/data/prism.db`. Backend seeds a `default` profile on st Target topology: backend on `127.0.0.1:8001`, frontend on `127.0.0.1:3001`, nginx terminates TLS and reverse-proxies. Code lives at `/var/www/prism-v2/` owned by `www-data`. -### 1. Initial install +Use `scripts/deploy.sh` on the server — it is idempotent and handles both initial install and redeploys. -```bash -sudo mkdir -p /var/www && sudo chown www-data:www-data /var/www -sudo -u www-data git clone /var/www/prism-v2 -cd /var/www/prism-v2 - -# Backend -sudo -u www-data python3 -m venv backend/.venv -sudo -u www-data backend/.venv/bin/pip install -r backend/requirements.txt - -# Frontend (writable npm cache required for www-data) -sudo mkdir -p frontend/.npm && sudo chown -R www-data:www-data frontend -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm \ - npm --prefix frontend ci -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm \ - npm --prefix frontend run build -``` - -### 2. systemd +### First-time setup on a fresh server ```bash -sudo cp systemd/prismv2-backend.service systemd/prismv2-frontend.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now prismv2-backend.service prismv2-frontend.service -``` +# As root / via sudo +mkdir -p /var/www && chown www-data:www-data /var/www +sudo -u www-data git clone /var/www/prism-v2 +cd /var/www/prism-v2 -### 3. nginx + TLS +# Install systemd units + nginx site + build + start +sudo ./scripts/deploy.sh --install -```bash -sudo cp nginx/prism.tylerhoang.xyz.conf /etc/nginx/sites-available/prism.tylerhoang.xyz -sudo ln -sf /etc/nginx/sites-available/prism.tylerhoang.xyz /etc/nginx/sites-enabled/ -sudo certbot --nginx -d prism.tylerhoang.xyz # first time only -sudo nginx -t && sudo systemctl reload nginx +# First-time TLS (Certbot edits the nginx server block in-place) +sudo certbot --nginx -d prism.tylerhoang.xyz +sudo systemctl reload nginx ``` -### 4. Redeploy +### Redeploy ```bash cd /var/www/prism-v2 -sudo -u www-data git pull origin master -sudo -u www-data backend/.venv/bin/pip install -r backend/requirements.txt -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm \ - npm --prefix frontend ci -sudo -u www-data env HOME=/var/www/prism-v2/frontend NPM_CONFIG_CACHE=/var/www/prism-v2/frontend/.npm \ - npm --prefix frontend run build -sudo systemctl restart prismv2-backend.service prismv2-frontend.service +sudo ./scripts/deploy.sh # pull + build + restart + smoke check +sudo ./scripts/deploy.sh --no-pull # rebuild + restart without git pull +sudo ./scripts/deploy.sh --install # also refresh systemd units / nginx site ``` +The script runs all build steps as `www-data` (with `HOME` + `NPM_CONFIG_CACHE` set), restarts both services, and curls `/health` on the backend and `/` on the frontend before exiting. + ### Ops ```bash diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..f192e72 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# +# Production deploy script for Prism v2. +# +# Runs on the production server (in /var/www/prism-v2). Idempotent — handles +# both first-time install and redeploys. +# +# Usage: +# sudo ./scripts/deploy.sh # full deploy (pull + build + restart) +# sudo ./scripts/deploy.sh --no-pull # build + restart only (skip git pull) +# sudo ./scripts/deploy.sh --install # also install systemd units + nginx site +# sudo ./scripts/deploy.sh --help + +set -euo pipefail + +APP_DIR="${APP_DIR:-/var/www/prism-v2}" +APP_USER="${APP_USER:-www-data}" +APP_GROUP="${APP_GROUP:-www-data}" +DOMAIN="${DOMAIN:-prism.tylerhoang.xyz}" +BACKEND_SVC="prismv2-backend.service" +FRONTEND_SVC="prismv2-frontend.service" + +DO_PULL=1 +DO_INSTALL=0 + +usage() { + sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' + exit 0 +} + +for arg in "$@"; do + case "$arg" in + --no-pull) DO_PULL=0 ;; + --install) DO_INSTALL=1 ;; + -h|--help) usage ;; + *) echo "Unknown arg: $arg" >&2; exit 2 ;; + esac +done + +log() { printf '\n=== %s ===\n' "$*"; } + +require_root() { + if [[ $EUID -ne 0 ]]; then + echo "deploy.sh must be run as root (use sudo)" >&2 + exit 1 + fi +} + +run_as_app() { + # Run a command as APP_USER with HOME + NPM_CONFIG_CACHE set so npm/git work. + sudo -u "$APP_USER" \ + env HOME="$APP_DIR/frontend" \ + NPM_CONFIG_CACHE="$APP_DIR/frontend/.npm" \ + "$@" +} + +require_root + +if [[ ! -d "$APP_DIR/.git" ]]; then + echo "Not a checkout: $APP_DIR (expected $APP_DIR/.git)" >&2 + echo "Clone the repo to $APP_DIR first." >&2 + exit 1 +fi + +cd "$APP_DIR" + +log "Ensuring ownership of $APP_DIR" +chown -R "$APP_USER:$APP_GROUP" "$APP_DIR" +mkdir -p "$APP_DIR/frontend/.npm" +chown -R "$APP_USER:$APP_GROUP" "$APP_DIR/frontend/.npm" + +if [[ $DO_PULL -eq 1 ]]; then + log "git pull origin master" + sudo -u "$APP_USER" git -C "$APP_DIR" pull --ff-only origin master +fi + +log "Backend: venv + dependencies" +if [[ ! -x "$APP_DIR/backend/.venv/bin/pip" ]]; then + sudo -u "$APP_USER" python3 -m venv "$APP_DIR/backend/.venv" +fi +sudo -u "$APP_USER" "$APP_DIR/backend/.venv/bin/pip" install --quiet --upgrade pip +sudo -u "$APP_USER" "$APP_DIR/backend/.venv/bin/pip" install --quiet -r "$APP_DIR/backend/requirements.txt" + +log "Frontend: npm ci + build" +run_as_app npm --prefix "$APP_DIR/frontend" ci +run_as_app npm --prefix "$APP_DIR/frontend" run build + +if [[ $DO_INSTALL -eq 1 ]]; then + log "Installing systemd units" + cp "$APP_DIR/systemd/$BACKEND_SVC" "/etc/systemd/system/$BACKEND_SVC" + cp "$APP_DIR/systemd/$FRONTEND_SVC" "/etc/systemd/system/$FRONTEND_SVC" + systemctl daemon-reload + systemctl enable "$BACKEND_SVC" "$FRONTEND_SVC" + + log "Installing nginx site" + cp "$APP_DIR/nginx/$DOMAIN.conf" "/etc/nginx/sites-available/$DOMAIN" + ln -sf "/etc/nginx/sites-available/$DOMAIN" "/etc/nginx/sites-enabled/$DOMAIN" + nginx -t + systemctl reload nginx +fi + +log "Restarting services" +systemctl restart "$BACKEND_SVC" "$FRONTEND_SVC" + +log "Smoke checks" +sleep 2 +if curl -fsS http://127.0.0.1:8001/health >/dev/null; then + echo " backend /health OK" +else + echo " backend /health FAILED" >&2 + echo " journalctl -u $BACKEND_SVC -n 50 --no-pager:" >&2 + journalctl -u "$BACKEND_SVC" -n 50 --no-pager >&2 || true + exit 1 +fi +if curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3001/ | grep -qE '^(200|301|302|307|308)$'; then + echo " frontend / OK" +else + echo " frontend / FAILED" >&2 + journalctl -u "$FRONTEND_SVC" -n 50 --no-pager >&2 || true + exit 1 +fi + +log "Deploy complete" +systemctl status "$BACKEND_SVC" "$FRONTEND_SVC" --no-pager --lines=0 || true -- cgit v1.3-2-g0d8e