Cloud Deployment - Fighter90/career-ops-ui GitHub Wiki

Running the whole stack in the cloud

Most people run career-ops on their own laptop. But the pipeline is at its best when it is always on โ€” scanning boards while you sleep, keeping the tracker fresh, reachable in a browser from any device. This page is the end-to-end recipe for putting the whole stack on a small cloud server: the parent career-ops pipeline, the career-ops-ui viewer, and the engine that runs the AI โ€” either your Claude subscription (through the Claude Code CLI) or a local Hermes gateway.

This is operator how-to, not an app feature. The complete, security-focused checklist is docs/integrations/HERMES.md; the in-app version is Help ยง31.

The three moving parts

Part What it is Where it lives
career-ops (parent) The AI job-search pipeline. Owns cv.md, config/, reports/, portals.yml. Driven by an agent CLI. the repo root
career-ops-ui (this app) A read-mostly web viewer that surfaces the same files in a browser; writes back only on explicit actions. career-ops/web-ui/
the engine Whatever answers the AI prompts โ€” a Claude subscription, a local Hermes gateway, or provider API keys. on the box (CLI / gateway) or a remote API

Two of the three are identical on a server and on your laptop โ€” only the engine choice and the network exposure change.

1. Provision and install

  • A small VPS (1 vCPU / 1 GB RAM is plenty for the viewer), current Linux.
  • Node โ‰ฅ 18 (22.5+ recommended โ€” it enables the parent's SQLite tracker index) and git.
  • Install exactly like a local install: clone the parent career-ops, then clone this repo inside it as career-ops/web-ui/.
  • Put provider keys in the parent's .env (never commit it โ€” .env / .env.* are gitignored; start from .env.example).
  • Run the viewer with npm start, bound to 127.0.0.1 โ€” you expose it through a proxy, not directly.

2. Pick your engine

career-ops is CLI-agnostic, so there are three honest options:

  • Your Claude subscription โ€” install the Claude Code CLI on the box and claude login with your Pro/Max plan. The parent's agent runs then use your subscription (no per-token API bill).
  • Hermes โ€” run hermes gateway on the same box (OpenAI-compatible API at http://127.0.0.1:8642/v1) and set HERMES_API_KEY in App settings. career-ops-ui's live evaluations route through it (last in the auto provider order).
  • API keys โ€” set any of the eighteen providers (Anthropic โ†’ Gemini โ†’ OpenAI โ†’ Qwen โ†’ OpenRouter โ†’ GitHub Models โ†’ Hermes โ†’ DeepSeek โ†’ GLM (Z.ai) โ†’ Kimi (Moonshot) โ†’ MiniMax โ†’ Mistral โ†’ Grok (xAI) โ†’ Together โ†’ Fireworks โ†’ Ollama โ†’ BytePlus Ark โ†’ Volcengine Ark) โ†’ Kimi (Moonshot) โ†’ MiniMax โ†’ Mistral โ†’ Grok (xAI) โ†’ Together โ†’ Fireworks โ†’ Ollama) in the parent .env, and the โšก live actions work headlessly.

You can mix them: a Claude subscription for the parent's heavy agent work, and a cheap or local provider for the viewer's quick evaluations.

3. Expose it safely

Moving off 127.0.0.1 means the safety loopback gave you for free must now be built explicitly โ€” the code is identical; only the exposure changes.

  • Keep the app on loopback; put a reverse proxy (nginx / Caddy) in front that terminates HTTPS (Let's Encrypt / automatic TLS) and forwards to 127.0.0.1:4317.
  • Run it under systemd or pm2 as a dedicated non-root user, Restart=on-failure.
  • Put authentication in front โ€” the app has no login of its own, so the proxy (basic-auth, SSO forward-auth, or a private network / VPN) is what keeps strangers out.
  • When you set HOST=0.0.0.0 so the proxy can reach it, the built-in hardening that was a no-op on loopback switches on and becomes load-bearing: the LLM rate-limit, the safeGet DNS-rebind defense, and path-name sanitizing.

Invariants that must survive the move (do not relax)

  • CSP โ€” no inline scripts, frame-ancestors 'none'. The proxy must not strip these headers.
  • SSRF guard โ€” every user-supplied URL fetch goes through isValidJobUrl() + safeGet (no loopback, no file://).
  • Markdown/XSS boundary โ€” stripDangerousMarkdown() server-side + escape-first UI.md() client-side.
  • No secrets in logs โ€” provider keys, tokens, PII never logged; a remote box with shipped logs makes this more important.
  • Parent read-only contract โ€” the server only reads cv.md / config/ / reports/ and writes on explicit actions.

See also