---
name: server-deploy
description: Safely deploy or publish a project's static files to Jonathan's self-hosted web server (oswaldjpickles.from-ca.com / 192.168.0.12), where many sites share one web root as subdirectories. Use when asked to deploy, publish, promote, push live, or update a site/page on the server. Handles backup-first, never disturbs other live sites, and verifies the deploy + neighboring sites afterward. Also covers the git-pull deploy model (preferred over scp once a project has a Gitea remote).
---

# Server Deploy

Deploy static site files to Jonathan's server **without disrupting any other live site**. The web root serves MANY projects as sibling subdirectories under one domain, so a careless deploy can clobber neighbors. Always back up, scope tightly, and verify.

## Critical safety rules

- **NEVER touch directories other than the target.** Many live sites share `~/fc-project/web/html/`. Deploy only into `<site>/` (or the single root `index.html`).
- **Back up before overwriting.** Always make a timestamped copy on the server first.
- **The root `index.html` is the project directory page** (see `project-directory` skill). Only replace it intentionally; back it up to `_archive/` first.
- **Verify after deploy:** the target returns 200 AND a few neighbor sites still return 200.
- Static files are served live from a bind mount via Caddy → no container restart needed; changes go live instantly.

## Server reference

- Host: `jonathan@192.168.0.12`  · SSH key: `~/.ssh/id_ed25519`
- Web root: `/home/jonathan/fc-project/web/html/`
- Archive dir: `/home/jonathan/fc-project/web/html/_archive/`
- Public URL: `https://oswaldjpickles.from-ca.com/<site>/`
- Caddyfile: `~/fc-project/Caddyfile` (catch-all `handle { reverse_proxy web:80 }` serves the static root)
- Note: some apps are reverse-proxied (Airron/Power/poker/Origin/Linguish/riposte/law/shipyard/MemMax) — those are NOT static-file deploys; do not scp over them.

## Workflow A — deploy a static site directory (`<site>`)

1. **Confirm the local source** (built `dist/`/`build/` or a vanilla HTML root) and the target `<site>` name (the URL path segment).

2. **Back up the existing server copy** (timestamped):
   ```bash
   ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 jonathan@192.168.0.12 \
     "cp -r ~/fc-project/web/html/<site> ~/fc-project/web/html/_archive/<site>.backup-\$(date +%Y%m%d%H%M%S) 2>/dev/null || true"
   ```

3. **Deploy with rsync** (trailing slash on source = copy contents). Prefer `rsync` over raw scp; exclude junk:
   ```bash
   rsync -avz --exclude '.git' --exclude 'node_modules' --exclude '.DS_Store' \
     -e "ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no" \
     ./path/to/local/dist/ jonathan@192.168.0.12:~/fc-project/web/html/<site>/
   ```

4. **Verify the target + neighbors** (all should be 200):
   ```bash
   ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 jonathan@192.168.0.12 '
   for u in <site>/ Wander/ PlateSupport/ Provisioner/queue.html; do
     curl -sk -o /dev/null -w "  /$u -> %{http_code}\n" "https://oswaldjpickles.from-ca.com/$u"
   done'
   ```

5. If the new site should appear on the landing page, regenerate the directory (see `project-directory` skill):
   `ssh jonathan@192.168.0.12 'bash ~/fc-project/gen-directory.sh'`

## Workflow B — replace the ROOT landing page (`index.html`)

Only when intentionally updating the project-directory homepage.

1. Back up the current root page:
   ```bash
   ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 jonathan@192.168.0.12 \
     "cp ~/fc-project/web/html/index.html ~/fc-project/web/html/_archive/index.backup-\$(date +%Y%m%d%H%M).html"
   ```
2. `scp` the new `index.html` to `~/fc-project/web/html/index.html`.
3. Verify root `/` is 200 **and** several subdir sites are still 200 (step 4 above).

## Workflow C — git-pull deploy (PREFERRED long-term)

Once a project has a Gitea remote (see `gitea-migrate` skill), deploy by pulling instead of scp — keeps server and git in sync.

1. First-time: clone the repo to a deploy path on the server (NOT over a live dir until verified).
2. Routine deploy:
   ```bash
   ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519 jonathan@192.168.0.12 \
     "cd <server-repo-path> && git fetch origin && git checkout main && git pull --ff-only origin main"
   ```
3. If the served files live elsewhere, rsync the repo's static output into the web-root `<site>/` (excluding `.git`).
4. Verify (step 4 above).

A per-project `deploy.sh` capturing the exact path + build (if any) is the goal; `platesupport` already has one to use as a template.

## Workflow D — deploy a SERVER APP (reverse-proxied: Next.js, Node, etc.)

For apps that need a running server (not static), e.g. Next.js (`next start`), Express:

1. **Subpath config first.** Set the framework's base/prefix so assets resolve under `/<Name>/`:
   - Vite: `base: '/<Name>/'` in `vite.config.ts`, then `npm run build` → deploy `dist/`.
   - Next.js: `basePath: '/<Name>'` in `next.config.ts`. The app then OWNS the prefix → in Caddy do **NOT** `uri strip_prefix` (unlike Origin which strips).
2. Deploy source to `~/apps/<name>` (rsync, exclude `node_modules .next .git`), then `npm install && npm run build` ON THE SERVER (build where it runs).
3. Run under **pm2**: `pm2 start npm --name <name> -- start` (pick a free port; existing: law-analyzer 3002, riposte 3001, reseller 4330). Then `pm2 save` so the enabled `pm2-jonathan` boot service resurrects it on reboot.
4. Add a Caddy route inside the `oswaldjpickles.from-ca.com {}` HTTPS block, BEFORE the catch-all `handle { reverse_proxy web:80 }`:
   ```
   handle /<Name>* { reverse_proxy 192.168.0.12:<port> }
   ```
5. Validate + reload: `docker exec fc-project-caddy-1 caddy validate --config /etc/caddy/Caddyfile` then `caddy reload ...`.

### ⚠️ CRITICAL: Caddyfile bind-mount inode trap
The Caddyfile is bind-mounted into the `fc-project-caddy-1` container. If you edit it by **replacing the file** (`awk ... > new && mv new Caddyfile`, or any tool that swaps the inode), the container keeps reading the OLD inode — your changes are invisible and requests fall through to the static `web:80` ("Not found"). Symptom: host `grep` shows your block, but `docker exec ... grep` shows count 0.
- **Fix / prevention:** edit the file **in place** (append with `>>`, or `sed -i`), OR after a replacing edit run `docker restart fc-project-caddy-1` to re-bind the current inode.
