summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-30 00:21:03 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-30 00:21:03 -0700
commit5fbc175e540803d919863f3d90dffc3c0645a90b (patch)
tree13713f582aa901311b2b2d05f289b351673f9827
parent66acc6f7d18c93f4b7960682bea5bd5ff1545802 (diff)
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 <noreply@anthropic.com>
-rw-r--r--AGENTS.md9
-rw-r--r--CLAUDE.md9
-rw-r--r--README.md54
-rwxr-xr-xscripts/deploy.sh124
4 files changed, 145 insertions, 51 deletions
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 <repo> /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 <repo-url> /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