Files
2026-10-19 21:22:09 +00:00

309 lines
14 KiB
Markdown

# Deploying Nextcraft — the admin runbook (v0.8)
**TL;DR** — AI-native outcome school + marketplace — graduates prove what they can build, not what they can write. This file explains how to stand the stack up on any Debian-based server: pick one of three install channels, run `nextcraft deploy --service`, and verify with the checklist; troubleshooting tables and evidence sit lower in the file.
Production topology since v0.3.6: **one port, one process**. The ai-service
(uvicorn, `:8420`) serves **both** the API and the exported web UI, so the
site works behind any proxy that can reach exactly one backend port — e.g.
the existing HAProxy frontend for `nextcraft.coreci.dev`.
```
browser ──HTTPS──▶ HAProxy :443 ──▶ 10.0.0.105:8420 (uvicorn)
├── /v1/* /health /docs → API routes
└── everything else → apps/web/out
```
No CORS, no mixed content, no second port. Same-origin pages fetch the API
with relative URLs (`NEXT_PUBLIC_AI_SERVICE_URL=self`).
## Pick your channel (v0.8)
nextcraft installs on any Debian-based Linux server through three channels.
Every channel ends at the same place: `nextcraft doctor` + `verify` green on
a running single-port stack. `nextcraft guide <topic>` carries the same
recipes offline inside the CLI.
| Channel | First install | Upgrades | Best for |
|---------|---------------|----------|----------|
| **docker** | `docker run` (image: API + UI pre-loaded) | `docker pull` + recreate | immutable, no host toolchain |
| **deb** | `dpkg -i` + `nextcraft deploy --service` | new `.deb` + `deploy --service` | apt-shaped fleets |
| **binary** | one-liner install + `deploy --service` | one-liner re-run + `deploy --service` | the v0.4 default path |
## Channel 1: docker (the image channel)
The image lives in the Gitea registry — every release pushes
`git.coreci.dev/coreci/nextcraft:<tag>` + `:latest`. It carries the whole
stack with all dependencies pre-loaded (web export baked, venv baked,
CLI embedded): no node, no pnpm, no repo clone on the host.
```sh
# First install (any Debian-based host with docker):
docker run -d --name nextcraft \
-p 8420:8420 \
-v nextcraft-state:/home/nextcraft/.nextcraft \
git.coreci.dev/coreci/nextcraft:<release-tag>
curl -fsS http://localhost:8420/health # {"status":"ok",...}
curl -fsS http://localhost:8420/ # HTML — the learner landing page
```
Point your proxy at `:8420` (HAProxy notes below). Durable state (SQLite,
sandbox workdirs, uploads) lives in the `nextcraft-state` volume — it
survives upgrades and recreation.
**The honest limit — learner sandbox builds:** the image runs unprivileged
by default and docker's default seccomp profile blocks the user namespaces
the sandbox fabric needs. The boot log says so loudly (`SANDBOX FABRIC
DEGRADED`), and env creation returns 503 with the same remediation text.
Options:
- run fully privileged where the host allows unprivileged userns:
`docker run ... --security-opt seccomp=unconfined --security-opt apparmor=unconfined`
- or run the stack binary-side on the host (channel 3) — the fabric works
natively there.
**In-container CLI** (the image embeds it at `/usr/local/bin/nextcraft`):
`doctor` and `verify` work (container-aware: export mount, venv import,
live mount probe); `guide` carries the runbook; `bootstrap`/`deploy`/`dev`
refuse honestly — the image is immutable.
**Upgrades:**
```sh
docker pull git.coreci.dev/coreci/nextcraft:<new-tag>
docker rm -f nextcraft
docker run -d --name nextcraft -p 8420:8420 \
-v nextcraft-state:/home/nextcraft/.nextcraft \
git.coreci.dev/coreci/nextcraft:<new-tag>
```
The state volume carries data across recreates. NEVER `docker exec
nextcraft deploy` — the immutable-image refusal is correct behavior.
[Backed by: the release-lane smoke gate (five assertions per release) +
`cli:test:e2e:docker` + the v0.7.4 live image probe.]
## Channel 2: deb (the apt-shaped channel)
Every release ships `nextcraft_<ver>_<arch>.deb` (+ `.sha256`) for amd64
and arm64. Contents: the self-contained CLI binary + docs; the postinst
runs an ADVISORY doctor — missing Recommends are reported, never fatal.
```sh
# First install:
curl -fsSL -o nextcraft.deb \
"https://git.coreci.dev/coreci/nextcraft/releases/download/<tag>/nextcraft_<ver>_amd64.deb"
dpkg -i nextcraft.deb # advisory doctor prints what deploy needs
apt install nodejs python3 git util-linux # the Recommends (deploy stack)
nextcraft doctor
nextcraft deploy --service
```
**Upgrades:** `dpkg -i nextcraft_<new-ver>_<arch>.deb` then `nextcraft
deploy --service` (idempotent). arm64 hosts: use the `arm64.deb`.
[Backed by: the release-lane deb smoke (fresh debian:bookworm install +
advisory postinst proof) + `cli:test:e2e:deb`.]
## Channel 3: binary (the v0.4 one-liner — unchanged)
```sh
# First install (linux x86_64 + aarch64/arm64 — the installer resolves
# your arch):
curl -fsSL https://git.coreci.dev/coreci/nextcraft/raw/main/scripts/install.sh | sh
nextcraft doctor # optional preflight (node, pnpm, python3, git, unshare)
nextcraft deploy --service
```
**Upgrades:** re-run the one-liner (idempotent — latest release binary,
sha256 + version-stamp gates) + `nextcraft deploy --service`.
[Backed by: the fresh-clone E2E (`cli:test:e2e`, v0.4) + install-script
fixture tests (tamper rejection, arch-resolve, both-arch matrix).]
## Upgrade matrix (summary)
| Channel | Upgrade | State |
|---------|---------|-------|
| docker | `docker pull` + recreate the container | persists in the `nextcraft-state` volume |
| deb | `dpkg -i` new deb + `nextcraft deploy --service` | `~/.nextcraft/` (host) |
| binary | install one-liner re-run + `deploy --service` | `~/.nextcraft/` (host) |
`nextcraft deploy --service` is the SAME command for deb + binary: it pulls
`origin/main` (or `--ref <tag>`), rebuilds the export, restarts, and
re-verifies. Inside docker these commands refuse honestly (image immutable).
## The one-command deploy (v0.4.6 — channels 2/3)
```sh
nextcraft deploy --service
```
That is the whole upgrade path AND the whole first-install path for the
host-side channels. In order it will:
1. **clone or update** the repo (default `~/nextcraft`; `--dir <path>` to
pick another location, `--ref <tag>` to pin a release)
2. **`pnpm install`** — the toolchain is *discovered*, not assumed:
node/pnpm behind nvm, PNPM_HOME, or corepack are found even when a
non-interactive shell can't see them (the v0.4.5-era outage was exactly
this: the box HAD pnpm, the deploy steps couldn't)
3. **ai-service venv** (`scripts/bootstrap.sh`)
4. **build the web export** (`NEXT_PUBLIC_AI_SERVICE_URL=self pnpm build`)
— always rebuilt; a stale or missing export is what serves the API's
JSON 404 at the domain root
5. **restart** — stops any old daemon, starts fresh
6. **verify** — venv, env, export presence, and the **live mount probe**:
with the service running, `GET :8420/` must return HTML, not the API's
JSON 404. The deploy fails loudly if the site isn't actually served.
`--service` additionally **installs a systemd unit** (generated with the
real clone + binary paths, no hand-editing) so the stack survives reboots
and crashes (`Restart=always`, `enable`d at install). Under systemd the
daemon commands are not used — `systemctl` is the supervisor:
```sh
systemctl status nextcraft-dev
systemctl stop nextcraft-dev # or: nextcraft stop
journalctl -u nextcraft-dev -f # or: nextcraft log -f (daemon mode)
```
Without `--service`, deploy uses the `dev -d` daemon (pidfile + log under
`~/.nextcraft/run/`) — fine for dev boxes, not boot-persistent.
## Manual mode (the underlying commands)
The deploy command composes the same steps you could run by hand:
```sh
cd /path/to/nextcraft
git pull --ff-only
pnpm install
NEXT_PUBLIC_AI_SERVICE_URL=self pnpm build
nextcraft stop && nextcraft dev -d # or: systemctl restart nextcraft-dev
nextcraft verify # live mount probe included
```
`nextcraft dev -d` auto-detects `apps/web/out` and exports
`AI_WEB_STATIC_DIR` to the server, so the UI is served with zero `.env`
editing. State lives in `~/.nextcraft/`:
| Path | Contents |
|------|----------|
| `~/.nextcraft/data/nextcraft.db` | SQLite (traces/grades/variants/defenses/identity) |
| `~/.nextcraft/sandboxes/` | sandbox workdirs (snapshot restore) |
| `~/.nextcraft/run/<clone-hash>/dev.log` | daemon log (daemon mode) |
| `~/.nextcraft/run/<clone-hash>/dev.pid` | daemon pidfile (daemon mode) |
Nothing durable is written inside the repo (v0.3.6 founder directive).
## Why static export
The web app is mock-data-driven with no server-side runtime needs, so
`pnpm build` emits plain HTML/JS/CSS to `apps/web/out` (`output: 'export'`).
uvicorn serves those files — there is no Node web server in production.
Cost: web changes need a `pnpm build` + service restart (no hot reload) —
`nextcraft deploy` does both.
## HAProxy notes
No config change is required — keep the single backend to :8420. Two
timeouts matter for streaming endpoints (SSE chat, LLM responses):
```
backend nextcraft_api
server api1 10.0.0.105:8420
timeout server 300s # matches the service's 300s LLM read budget
timeout tunnel 3600s # long-lived streams
```
The in-sandbox telemetry capture agents do NOT traverse HAProxy: they dial
`ws://127.0.0.1:8420` on the host's loopback directly.
## Fresh start on old boxes (v0.3.6 migration)
The pre-v0.3.6 defaults kept the SQLite DB and sandbox workdirs **inside
the repo**. On upgrade, those old locations are simply abandoned (fresh
start per founder decision) — delete them at leisure:
```sh
rm -rf apps/ai-service/ai_service/data apps/ai-service/sandboxes
```
Orphaned sandbox workdirs under the NEW `~/.nextcraft/sandboxes/` are
reaped automatically on service startup (a-1 startup reaper).
## v0.5 note: pre-v0.5 databases (fresh start per the deploy directive)
The deployed box runs a FRESH `~/.nextcraft` state (v0.3.6 directive), so
nothing below applies there. For any other box carrying a pre-v0.5
`~/.nextcraft/data/nextcraft.db`: v0.5 adds two `variant_record` columns
(`environment`, `test_command`). The variant store backfills them
automatically on first open (idempotent `ALTER TABLE`; pre-v0.5 rows read
as build-kind, test commands fall back to `pytest` at the API seam), so
an in-place upgrade is safe — no schema migration step is required.
Alternatively, follow the fresh-start convention and move the old DB
aside (`mv ~/.nextcraft/data/nextcraft.db{,.pre-v05.bak}`); it
is regenerated on boot.
## Verification checklist
- `curl -fsS https://nextcraft.coreci.dev/health` → `{"status":"ok",...}`
- `curl -fsS https://nextcraft.coreci.dev/` → HTML (the learner landing page)
- `nextcraft verify` → all checks pass (including the live mount probe)
- An unknown path returns the site's 404 page with status 404
- `systemctl status nextcraft-dev` shows active (or `nextcraft log -f`
shows the uvicorn access log in daemon mode)
- After a reboot: the service comes back by itself (systemd mode)
## Troubleshooting (v0.8 extension — the four tables)
### Site serves JSON 404 at `/`
The web export is missing or the mount is unset. Binary/deb: `nextcraft
deploy --service` rebuilds the export and re-verifies. Docker: the image
bakes the export at `/opt/nextcraft/web-out` — check nothing overrides
`AI_WEB_STATIC_DIR`. [Backed by: the live mount probe in `verify` — the
check that caught every prior outage.]
### Port 8420 busy
Binary/deb: `nextcraft stop` (or `systemctl stop nextcraft-dev`). Docker:
the `-p` map is yours — fix the conflict or change the host side of the map.
### `pnpm not found` during deploy
The toolchain discovery probes PATH, nvm, PNPM_HOME, corepack. If still
missing: `corepack enable pnpm`. Docker channel: not applicable (the
toolchain is baked).
### Sandbox degrade notice (docker channel)
Expected unprivileged behavior — the fabric needs user namespaces and
docker's default seccomp blocks them. The boot notice and the 503 payload
name the two options (privileged flags / host-side stack); see Channel 1
above for the exact flags. NOT a bug; a 201 there would be a bug.
### Checksum mismatch on install
Do NOT run the binary. Delete it, re-download, verify again; report if it
persists. The installer hard-stops by design (sha256 + version-stamp
gates). [Backed by: install-script tamper-rejection fixture tests.]
### Runner / CI questions
The runner fleet lives on CT 113 (`deploy/ci/` is the committed
fleet-as-code — compose + configs + prune cron). Every push runs the
node+python lanes; every release is machine-made by the tag-triggered
workflow (release.yaml). A red Actions tab on a tag = a red release by
design (the smoke gate).
## Evidence appendix (which test or live probe backs each recipe)
| Recipe | Backing |
|--------|---------|
| docker first-install + degrade + refusals | release-lane smoke gate (5 assertions, every release) + `cli:test:e2e:docker` + the v0.7.4 live image probe (HEALTH/GUIDE/REFUSAL all green) |
| deb install + advisory doctor | release-lane deb smoke (fresh debian:bookworm per release) + `cli:test:e2e:deb` |
| binary install (both arches) | `cli:test` install-script fixtures: checksum tamper rejection, version-stamp gate, arch-resolve, both-arch matrix |
| one-command deploy + live mount probe | the v0.4 fresh-clone E2E (`cli:test:e2e`) + the deployed live box (nextcraft.coreci.dev 200 since v0.4.6) |
| honest degrade text (boot notice = 503 payload) | `tests/sandbox/test_capability_degrade.py` (G-208 no-drift pinned by test) |
| guide ↔ runbook parity | the guide's channel sections mirror this file's Channels 1-3 (kept in agreement by review at P5 ship; `guide` tests pin the channel markers) |
| release machine-made (assets + image) | every release from v0.7.1 onward: binary + checksums + debs attached + image pushed by `.gitea/workflows/release.yaml` |