# Deploy ProxMate with Claude Code This file is an **executable runbook for an AI agent** (Claude Code). Point Claude Code at it and it will stand up a production-ready ProxMate — the same shape as a hand-built deployment: HTTPS, tenant isolation, and (optionally) the in-guest IDE — while stopping to confirm every action that touches production or handles a secret. > **How a human invokes this:** open Claude Code in a clone of this repo (or on the target > server) and say: *"Follow DEPLOY_WITH_CLAUDE.md to deploy ProxMate."* Claude Code reads > this file and drives the steps below. **You (the operator) stay in the loop** — you approve > each production action and you type every secret yourself. The canonical human runbook is [`DEPLOYMENT.md`](DEPLOYMENT.md); this file mirrors it as agent instructions. If they ever disagree, `DEPLOYMENT.md` wins — tell the operator. --- ## 0. Agent safety contract (read first, follow always) You are deploying software that will run someone's infrastructure. Behave accordingly: 1. **Confirm before every production-changing or irreversible action** — enabling the cluster firewall, `docker compose up`, editing a live `.env`, rebooting a VM, running a migration. State exactly what you're about to run and wait for a clear "yes". 2. **Never handle secrets in the clear.** Do not ask the operator to paste passwords, Proxmox token secrets, SMTP passwords, or the `ENCRYPTION_KEY` into the chat. Generate `ENCRYPTION_KEY` on the host with `openssl rand -hex 32` and have the operator paste secrets **directly into the `.env` file or the browser setup wizard**, not to you. Never echo a secret back. 3. **Never commit `.env`** (or `backend/.env`) or any file containing a secret. They are gitignored; keep it that way. 4. **Do not weaken security to make something work.** If a step fails, diagnose it — do not disable the firewall, set `COOKIE_SECURE=false` in production, open app ports to the world, or set `ALLOW_PRIVATE_OUTBOUND_URLS=true` unless the operator explicitly needs LAN webhooks. 5. **Stop and ask** whenever a decision is the operator's: the domain, the isolation model, whether to expose ports vs. use a tunnel, whether to enable the in-guest IDE. 6. **Verify, don't assume.** After each phase, run the check and report the real result. If a command fails, show the output; never claim success you didn't observe. If any instruction here conflicts with the safety contract, the contract wins. --- ## 1. Gather these from the operator (ask, then confirm back) Collect this before touching anything. Ask as a short questionnaire. - **Target host:** are we deploying on THIS machine, or over SSH to a remote host? (If remote, confirm you can reach it and that it's the right box.) - **Public domain** for ProxMate, e.g. `proxmate.example.com`, with DNS already pointing at the host. - **HTTPS strategy:** (a) open ports 80/443 with **Caddy** auto-TLS, or (b) a **Cloudflare Tunnel** (no open ports, TLS at the edge, plain HTTP to a local merge-proxy). Pick one. - **Proxmox:** API host URL (`https://:8006`), and confirm a dedicated **API token** will be created with **privilege separation OFF** (see §2). Self-signed cert? (verify-SSL off in OOBE.) - **Tenant isolation model:** shared bridge + per-VM firewall, or a dedicated VLAN/SDN VNet (recommended). The operator must confirm the **management network CIDR** (Proxmox 8006 + SSH) so isolation enforcement doesn't lock them out. - **App-database backup directory:** a host path for ProxMate's own nightly DB snapshots (`PROXMATE_BACKUP_DIR`). Prefer a different physical disk from the database. Also ask **where the `ENCRYPTION_KEY` backup will live — off the host** (see §9); backups are worthless without it. - **Optional:** SMTP relay (password-reset email **and** the compute-access-window warning emails — see §6.1), Keycloak OIDC (SSO), and whether to enable the **in-guest IDE** (§7). Repeat the collected answers back and get a "go" before Phase 2. --- ## 2. Preflight (host + Proxmox) Run these read-only checks and report results: ```bash docker --version && docker compose version # Compose v2 required docker ps -q >/dev/null && echo daemon-ok # versions alone don't prove daemon ACCESS — # "permission denied" here means the deploy user # needs the docker group (usermod -aG docker , re-login) # DNS: does the domain resolve to this host's public IP? getent hosts || true # Can the host reach the Proxmox API? curl -sS -o /dev/null -w '%{http_code}\n' -k https://:8006/api2/json/version ``` **Proxmox API token (the #1 pitfall).** A privilege-separated token has NO permissions — storage lists come back empty and VM creation 403s. Have the operator run, on a Proxmox node as root: ```bash pveum user token add root@pam proxmate --privsep 0 # → copy the displayed token VALUE now (shown once). Token ID is: root@pam!proxmate ``` A privsep-off token covers what ProxMate needs, including setting guest tags (`VM.Config.Options` — every managed guest is tagged with its machine) and, on Proxmox VE 8+, `SDN.Use` / `SDN.Audit`. A narrower token must hold those too; the connection test warns when the SDN privileges are missing. Tell the operator to keep that secret for the browser wizard in §5 — **do not** have them paste it to you. --- ## 3. Code + environment ```bash # If not already in a clone: git clone proxmate && cd proxmate cp .env.docker.example .env openssl rand -hex 32 # ENCRYPTION_KEY — operator pastes THIS into .env, keeps a backup chmod 600 .env ``` Have the operator edit `.env` (you may open it and explain fields, but they fill secrets). The production block, per `DEPLOYMENT.md` §4: | Variable | Value | Why | |---|---|---| | `ENCRYPTION_KEY` | the 64-hex string | Encrypts Proxmox token + all secrets at rest. **Back it up; keep it stable.** | | `FRONTEND_URL` | `https://` | CORS + redirects | | `NEXT_PUBLIC_API_URL` | `https:///api` | Baked into the browser bundle at build time | | `NEXT_PUBLIC_SITE_URL` | `https://` | Public origin for SEO/OG/canonical/manifest metadata. **Also baked in at build time** — changing it needs a frontend image rebuild | | `BACKEND_PUBLIC_URL` | `https://` | SSO callback base — and the **IDE gateway base URL** (must be https, see §7) | | `WEBAUTHN_RP_ID` / `WEBAUTHN_ORIGIN` | `` / `https://` | Passkeys | | `COOKIE_SECURE` | `true` | Secure cookies (HTTPS) | | `TRUST_PROXY` | `1` | One proxy hop → real client IP for rate-limiting and the audit log | | `BIND_ADDR` | `127.0.0.1` | Only the local reverse proxy reaches the app ports | | `REAL_IP_HEADER` | `x-forwarded-for` with Caddy/nginx; leave unset behind Cloudflare | Which header carries the real client IP for the audit log (unset = `cf-connecting-ip`) | | `METRICS_TOKEN` | a random string | `/metrics` returns 404 in prod without it; scrape with `Authorization: Bearer ` | | `PROXMATE_BACKUP_DIR` | e.g. `/mnt/backups/proxmate-db` | Host directory for ProxMate's own nightly **database** backups. The container path is fixed at `/var/backups/proxmate`; this picks the host side. Creating a folder on the host does nothing unless it's set here | Confirm `COOKIE_SECURE=true` (passkeys and `SameSite` cookies need a secure context) and `BACKEND_PUBLIC_URL` is your **https** origin — since v0.8.0 the IDE gateway URL is built from that value outright rather than sniffed from forwarded headers, which proved unreliable behind a proxy that rewrites `X-Forwarded-Proto`. `TRUST_PROXY=1` matters for correct client IPs in rate-limiting and the audit log. --- ## 4. TLS / reverse proxy Follow the operator's choice from §1. - **Caddy (open ports 80/443):** install Caddy, `sudo cp deploy/Caddyfile /etc/caddy/Caddyfile`, set the domain + ACME email, `sudo systemctl reload caddy`. Confirm 80/443 are reachable and DNS resolves so ACME can issue a cert. - **Cloudflare Tunnel (no open ports):** run a small no-TLS merge-proxy (see `DEPLOYMENT.md` §5 "Cloudflare Tunnel") that joins `/api` → `127.0.0.1:4000` and `/` → `127.0.0.1:3000` on one local port, then point the tunnel's public hostname at it. **WebSockets must pass through** (the noVNC console AND the IDE both need this — verify after launch). Either way there is **one HTTPS origin**; the app ports stay on `127.0.0.1`. --- ## 5. Build, launch, first run **Confirm with the operator, then:** ```bash docker compose up -d --build docker compose ps docker compose logs -f backend # watch "Applying database migrations..." then "Starting ProxMate API..." ``` The backend entrypoint runs `npx prisma migrate deploy` on boot, so schema + all migrations apply automatically. Then the operator opens `https://` and completes the **setup wizard**: creates the owner account, enters the Proxmox host + token (from §2), sets VM defaults (storage, bridge — point at the tenant VLAN/VNet), ISO storage. **The operator does this in the browser**; you just confirm the app is up and healthy: ```bash curl -sS https:///api/health # expect {"status":"ok",...} ``` --- ## 6. Tenant isolation enforcement (do NOT skip) ProxMate applies a per-VM firewall to every tenant VM (`policy_in=DROP`, mac-filter, RFC1918 drop, DNS allowed) — but the rules only take effect once the Proxmox **cluster firewall** is on. - Have the operator go to **admin Settings ▸ Proxmox tab ▸ Tenant network isolation ▸ Enable enforcement** (deep link: `/admin/settings?tab=proxmox`). ProxMate adds management allow-rules (8006 + SSH on the confirmed mgmt CIDR) **before** flipping the datacenter firewall, so they don't lock themselves out. **Confirm the suggested CIDR matches their mgmt network.** - Verify: Proxmox web (8006) + SSH still reachable; two tenant VMs cannot reach each other. Do not proceed to the IDE until isolation is enforced and verified. ### 6.1 Compute access windows (decide before inviting anyone) Invites can grant a **fixed term** of cluster access or **never expire**. The term is anchored at sign-up, and an admin can override it per user afterwards under **Settings ▸ Access**. State this to the operator so the behaviour isn't a surprise: - Expiry **suspends, never deletes** — the tenant's VMs are stopped and sign-in is refused across every auth path (including the console WebSocket, the IDE proxy, and both token families), but **nothing is destroyed** and an admin can extend the window to restore access. - Warning emails go out **7 days and 1 day** before a window closes, and admins get an `access.expired` notification — all of which need **SMTP configured (§8)**. Without SMTP the suspension still happens, silently. - **Admins never expire.** A sweep runs hourly (at :20), with a catch-up shortly after boot, so a window that lapsed while the host was down is still enforced. Confirm with the operator: default invite duration, and whether existing invites should be never-expiring. --- ## 7. ProxMate IDE — production setup (optional) The in-guest IDE (browser code-server + an OpenCode AI agent per VM) has extra requirements. Skip this whole section if the operator doesn't want it; the rest of ProxMate is complete without it. ### 7.1 Requirements to confirm with the operator - **The ProxMate host must be able to reach tenant VM IPs on TCP :8080** (or `IDE_GUEST_PORT` if set in `.env`). On a **flat** network (ProxMate on the same LAN as the guests) this is automatic. On a **non-flat** network (e.g. ProxMate in a container whose network can't reach the guest VLAN) the operator must provide routing — a **Tailscale subnet route**, a VPN, or a route — so the backend can reach `guest-ip:8080`. This is the operator's networking to solve; ProxMate can't create it. - **`ide_ingress_cidr`** — when tenant isolation is on, ProxMate opens a single **managed, infra-scoped** `:8080` firewall pinhole on each IDE VM. It must be the address ProxMate's traffic actually arrives from (the backend host on a flat LAN; the **subnet-router node's LAN IP** when routed). Set it in admin IDE settings (or the `ide_ingress_cidr` SystemConfig). **Never a wildcard** — the code rejects `0.0.0.0/0`. - **Guest agent + guest specs:** IDE VMs need the QEMU **guest agent** running (the install goes through it), **>= 8 GB RAM** (the default `ide_min_ram_mb` floor — the install OOMs a 4 GB box), and a CPU that exposes **AVX** (the OpenCode/Bun runtime needs it). ProxMate auto-sets `cpu: host` at IDE-enable and, if AVX is still masked, offers the remedy the node dictates: a reboot when the node's silicon has AVX (or is unknown), or a **one-click relocate to an AVX-capable node** when the node's CPU demonstrably lacks it. - **LLM models:** in admin IDE settings, add a model **source** (an OpenAI-compatible endpoint such as a local Ollama — admins may use a LAN address) and share models to tenants, and/or allow tenants to bring their own keys — since v0.8.0 tenants may only use OpenAI or the fixed preset OpenAI-compatible bases (OpenRouter, Groq); a free-form custom base URL is **admin-only**. ### 7.2 Enable + verify - Admin **Settings ▸ ProxMate IDE**: set the tier (off / admin / tenants), the model source, shared models + visibility, and `ide_ingress_cidr`. - On a test VM (>= 8 GB, guest agent running), click **Open IDE**. ProxMate installs code-server + OpenCode into the guest, opens the editor, and wires the AI agent through the gateway. Confirm: the terminal shows the VM's own hostname; the AI agent answers a prompt (that proves the whole chain: reachability, firewall pinhole, https gateway URL, model routing). ### 7.3 IDE security model (state this to the operator) - The proxy is **owner-only**: every request is resolved through `getVmWithCap(vmId, user, 'ide')` and must come from the VM's owner — admins and share holders are refused. It refuses loopback/link-local targets, strips ProxMate's cookies from proxied requests, and relays only to a `code-server` that proves the VM's own per-install secret. IDEs installed by 1.0.x reinstall once, automatically, on their owner's next open. - The `:8080` pinhole is **infra-scoped only** — the guest keeps `policy_in=DROP`, so tenants stay isolated from each other; only ProxMate's address can reach code-server. - The LLM gateway enforces an **admin allow-list** (a tenant can't reach an un-shared model or, when disabled, any BYO key), the token is **per-VM** and never in the guest config in cleartext beyond an env file, and tenant BYO endpoints are restricted to an **allow-list of preset providers** (OpenAI / OpenRouter / Groq) — free-form custom base URLs are admin-only — on top of the SSRF guard. --- ## 8. Final verification checklist Report each as pass/fail with evidence: - [ ] `https://` loads on a valid cert; `/api/health` returns `ok`. - [ ] OOBE done, Proxmox connected; a test VM created, console (noVNC WebSocket) opened, deleted. - [ ] Tenant isolation enforced; mgmt (8006/SSH) still reachable; two tenants isolated. - [ ] (If enabled) IDE opens on a test VM, lands on the right guest, AI agent answers. - [ ] `.env` is `chmod 600`, not committed; `ENCRYPTION_KEY` backed up by the operator. - [ ] App ports (3000/4000) are NOT reachable from the internet; only 80/443 (or the tunnel) are. --- ## 9. Rollback / recovery - **Back up ProxMate's own database before any change.** ProxMate snapshots its DB nightly (`VACUUM INTO`, consistent on a live database) into `PROXMATE_BACKUP_DIR` with rolling retention — force one from **Settings ▸ Maintenance ▸ Back up now**. A manual volume snapshot still works as a belt-and-braces alternative: `docker run --rm -v proxmate_proxmate-data:/data -v "$PWD":/backup alpine tar czf /backup/proxmate-db-$(date +%F).tgz -C /data .` - **A database backup is useless without `ENCRYPTION_KEY`.** Every stored secret (Proxmox token, SMTP password, TOTP secrets) is AES-256-GCM encrypted under it. Keep one copy **off the host** — it never changes, so a password-manager entry is enough. - **Bad deploy:** `git checkout && docker compose up -d --build` (migrations are forward-only; restore the DB volume backup if a migration must be undone). Note the backend entrypoint runs `prisma migrate deploy` itself — never run migrations by hand, and never via `docker compose run` (it inherits the entrypoint and hangs booting a second server). - **Locked out by the firewall:** Proxmox VM/firewall config lives on shared pmxcfs — fix it from a healthy node (`/etc/pve/firewall/…`), or disable the datacenter firewall from a node console. --- *Companion docs: [`DEPLOYMENT.md`](DEPLOYMENT.md) (human runbook), [`SECURITY.md`](SECURITY.md) (threat model + tenant isolation), [`docs/admin-guide.md`](docs/admin-guide.md), and the in-repo IDE docs.*