# CACT server deploy playbook Repeatable runbook for standing up the **CloudAxe Coder Team (CACT)** on a new Linux server: headless OpenCode, the `cact-*` agent pack, provider credentials, systemd supervision, and a CloudAxe desktop connection check. Use placeholders only. Do not commit live IPs, passwords, or API keys. ## What you are deploying | Piece | Role | | --- | --- | | OpenCode headless server | `opencode serve` HTTP API on port `4096` | | CACT agents | `~/.config/opencode/agents/cact-*.md` | | Global rules | `~/.config/opencode/AGENTS.md` | | Provider auth | `~/.local/share/opencode/auth.json` (never in git) | | Clients | CloudAxe / OpenCode Desktop (Settings → Add Server), or `curl` / SDK | `cact-` means **CloudAxe Coder Team**. The normal entry agent is `cact-orchestrator-tech-lead`. Paths follow XDG defaults (`xdg-basedir`): | Kind | Default path (Linux) | | --- | --- | | Config | `~/.config/opencode` | | Data (auth, logs) | `~/.local/share/opencode` | | State | `~/.local/state/opencode` | | Cache | `~/.cache/opencode` | Override the config directory with `OPENCODE_CONFIG_DIR` only when you intentionally isolate installs. ## Assumptions - Target OS: Ubuntu 22.04/24.04 or Debian 12 (amd64 or arm64). - You have SSH access and can use `sudo`. - You will use a **dedicated service user** (example: `opencode`). - The CloudAxe desktop expects the **same OpenCode build** as this fork (v2 API). Do not deploy an unrelated older package and assume compatibility. - Provider policy: **direct vendor APIs** (Moonshot, DeepSeek, Z.ai). Avoid OpenRouter for CACT agents. ## Pre-flight Before changing anything on the host: 1. Confirm access scope (sudo / root). 2. Name the environment: `dev` / `staging` / `production`. 3. Note existing listeners on `4096` (`ss -tlnp | grep 4096`). 4. Decide whether port `4096` is exposed raw or only via Nginx TLS on `443`. 5. Prepare a rollback posture: previous binary path, config tarball location, and how to stop the unit. Destructive classification for a first install: **risky** (new systemd unit, firewall rules, public listener). Treat password/key rotation and unit replacement on a live host as **destructive** unless a verified backup and rollback path exist. --- ## 1. Target shape ```text Internet / operator LAN │ ▼ [optional Nginx TLS :443] │ ▼ opencode serve --hostname 0.0.0.0 # or 127.0.0.1 behind Nginx --port 4096 + OPENCODE_SERVER_PASSWORD + OPENCODE_SERVER_USERNAME (optional; default opencode) │ ▼ ~/.config/opencode/agents/cact-*.md ~/.local/share/opencode/auth.json ``` CLI flags (see `packages/opencode/src/cli/cmd/serve.ts` and `packages/opencode/src/cli/network.ts`): | Flag | Purpose | Typical CACT value | | --- | --- | --- | | `--port` | Listen port | `4096` | | `--hostname` | Bind address | `0.0.0.0` (direct) or `127.0.0.1` (behind proxy) | | `--cors` | Extra browser origins | Desktop / app origins as needed | | `--mdns` | LAN discovery | **off** on a VPS | If `OPENCODE_SERVER_PASSWORD` is unset, the server starts **unsecured** and prints a warning. Always set it for any network-reachable host. --- ## 2. Host bootstrap Run as a sudo-capable operator. ### 2.1 Packages ```bash sudo apt-get update sudo apt-get install -y git curl ca-certificates ripgrep build-essential unzip ``` Install Bun (required to build/run from this repo): ```bash curl -fsSL https://bun.sh/install | bash # ensure ~/.bun/bin is on PATH for the service user as well ``` ### 2.2 Service user ```bash sudo useradd --system --create-home --shell /bin/bash opencode sudo -u opencode -H bash -lc 'mkdir -p ~/.config/opencode/agents ~/.local/share/opencode' ``` ### 2.3 Clone and install this fork Replace `YOUR_FORK_URL` and pin a known-good ref (tag or commit SHA). ```bash sudo -u opencode -H bash <<'EOF' set -euo pipefail cd ~ git clone YOUR_FORK_URL opencode cd opencode git checkout YOUR_PINNED_REF bun install cd packages/opencode bun run build # or the package's documented production build script EOF ``` Install a durable binary on `PATH` for the service user (example: copy the built CLI into `~/bin`): ```bash sudo -u opencode -H bash <<'EOF' set -euo pipefail mkdir -p "$HOME/bin" # Adjust source path to match the build output for your platform cp -f "$HOME/opencode/packages/opencode/dist/opencode-linux-"*/bin/opencode "$HOME/bin/opencode" chmod 755 "$HOME/bin/opencode" "$HOME/bin/opencode" --version EOF ``` Record the version string in your ops notes. The CloudAxe desktop must speak the same API generation as this server. Alternative: install a prebuilt musl binary that matches this fork’s release artifacts (see `packages/opencode/Dockerfile`), then still deploy CACT config as below. --- ## 3. Deploy the CACT agent pack Copy from a trusted workstation or private backup into the service user’s config. Source of truth on the operator machine is typically: ```text ~/.config/opencode/AGENTS.md ~/.config/opencode/agents/cact-*.md ``` On the server: ```bash # From an operator machine (example) scp AGENTS.md opencode@SERVER:~/.config/opencode/ scp cact-*.md opencode@SERVER:~/.config/opencode/agents/ ``` Expected agents: | Agent file | Model (frontmatter) | Provider | | --- | --- | --- | | `cact-orchestrator-tech-lead.md` | `moonshotai/kimi-k3` | Moonshot | | `cact-frontend-agent.md` | `moonshotai/kimi-k3` | Moonshot | | `cact-backend-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-database-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-devops-server-engineer.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-security-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-qa-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-gatekeeper-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-requirements-agent.md` | `zai/glm-5.2` | Z.ai | | `cact-requirements-agent-glm-divergent.md` | `zai/glm-5.2` | Z.ai | | `cact-research-agent.md` | `zai/glm-5.2` | Z.ai | | `cact-research-agent-glm-divergent.md` | `zai/glm-5.2` | Z.ai | | `cact-requirements-agent-deepseek.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-research-agent-deepseek.md` | `deepseek/deepseek-v4-pro` | DeepSeek | | `cact-documentation-agent.md` | `openrouter/moonshotai/kimi-k2.6` | **Needs fix** | **OpenRouter callout:** `cact-documentation-agent` still references OpenRouter. Before relying on that agent, change its frontmatter `model` to a direct Moonshot (or other approved direct) model ID and verify with `opencode models --refresh` on the server. Optional global config (`~/.config/opencode/opencode.jsonc`) may enable MCP servers. Keep secrets out of that file. Do **not** copy `auth.json`, `.env`, or systemd env files into the git repository. --- ## 4. Secrets and provider auth ### 4.1 Server basic auth env file ```bash sudo install -d -m 0750 -o root -g opencode /etc/opencode sudo tee /etc/opencode/server.env >/dev/null <<'EOF' OPENCODE_SERVER_USERNAME=opencode OPENCODE_SERVER_PASSWORD=REPLACE_WITH_LONG_RANDOM_SECRET # Optional: pin HOME for the service if the unit does not set User= # HOME=/home/opencode EOF sudo chmod 0640 /etc/opencode/server.env sudo chown root:opencode /etc/opencode/server.env ``` Generate a strong password (`openssl rand -base64 32`). Store it in your password manager; do not paste it into tickets or git. ### 4.2 Provider credentials As the service user, log in to each vendor API you need: ```bash sudo -u opencode -H bash -lc 'export PATH="$HOME/bin:$HOME/.bun/bin:$PATH"; opencode auth login' ``` Credentials land in `~/.local/share/opencode/auth.json` under that user’s home. Restrict permissions: ```bash sudo -u opencode -H bash -lc 'chmod 600 ~/.local/share/opencode/auth.json' ``` Minimum providers for the current CACT pack: Moonshot, DeepSeek, Z.ai. Refresh model lists after login: ```bash sudo -u opencode -H bash -lc 'opencode models --refresh' ``` --- ## 5. systemd unit Create `/etc/systemd/system/opencode-cact.service`: ```ini [Unit] Description=OpenCode CACT headless server After=network-online.target Wants=network-online.target [Service] Type=simple User=opencode Group=opencode WorkingDirectory=/home/opencode EnvironmentFile=/etc/opencode/server.env Environment=HOME=/home/opencode Environment=PATH=/home/opencode/bin:/home/opencode/.bun/bin:/usr/local/bin:/usr/bin ExecStart=/home/opencode/bin/opencode serve --hostname 0.0.0.0 --port 4096 Restart=on-failure RestartSec=5 # Harden lightly; expand after validating tool/workspace needs NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target ``` If you terminate TLS at Nginx, bind OpenCode to loopback instead: ```ini ExecStart=/home/opencode/bin/opencode serve --hostname 127.0.0.1 --port 4096 ``` Enable and start: ```bash sudo systemctl daemon-reload sudo systemctl enable --now opencode-cact.service sudo systemctl status opencode-cact.service --no-pager sudo journalctl -u opencode-cact.service -n 50 --no-pager ``` ### Rollout order 1. Validate binary: `opencode --version` 2. Validate env file permissions and that `OPENCODE_SERVER_PASSWORD` is set 3. `daemon-reload` → `restart` (or `start` on first install) 4. Run health probes (section 7) 5. Connect desktop ### Rollback 1. `sudo systemctl stop opencode-cact.service` 2. Restore previous binary into `/home/opencode/bin/opencode` 3. Restore config/auth from the timestamped snapshot (section 8) 4. `sudo systemctl start opencode-cact.service` 5. Re-run health probes --- ## 6. Network and hardening ### 6.1 Firewall Allow `4096/tcp` only from operator addresses (example with UFW): ```bash sudo ufw allow OpenSSH sudo ufw allow from OPERATOR_CIDR to any port 4096 proto tcp sudo ufw enable sudo ufw status verbose ``` Cloud security groups / nftables should match the same allowlist. ### 6.2 Optional Nginx TLS When the host must not expose raw HTTP: 1. Bind OpenCode to `127.0.0.1:4096`. 2. Proxy `https://cact.example.com` → `http://127.0.0.1:4096` with WebSocket upgrade headers if clients use SSE/WS. 3. Obtain certificates (Let’s Encrypt / ACME). 4. Point the desktop at `https://cact.example.com` with the same basic-auth username/password. Keep mDNS disabled on public VPS hosts (`--mdns` unset / false). ### 6.3 Workspace directories Clients send a project directory via request headers. Ensure the service user can read/write the repos you intend to work in (for example under `/home/opencode/workspaces`). Do not run the server as root. --- ## 7. Prove it Replace placeholders. Username defaults to `opencode` unless you set `OPENCODE_SERVER_USERNAME`. ### 7.1 Authenticated HTTP ```bash export CACT_URL='http://SERVER_IP:4096' export CACT_USER='opencode' export CACT_PASS='YOUR_SERVER_PASSWORD' # Global health (many builds) curl -fsS -u "$CACT_USER:$CACT_PASS" "$CACT_URL/global/health" # v2 location probe (desktop also uses this when health routes are odd) curl -fsS -u "$CACT_USER:$CACT_PASS" "$CACT_URL/api/location" # OpenAPI explorer (optional) # open "$CACT_URL/doc" ``` Desktop health checking (`packages/app/src/utils/server-health.ts`) tries, in order: 1. `/api/health` 2. `/global/health` 3. Successful `/api/location` (treat as healthy when health routes fail for a non-network reason) A 404 on one health path with a working `/api/location` can still show **online** in CloudAxe Desktop. ### 7.2 Agent inventory on the host ```bash sudo -u opencode -H bash -lc 'opencode agent' # or the install's list-agents command # Confirm cact-orchestrator-tech-lead and other cact-* names appear ls -1 /home/opencode/.config/opencode/agents/cact-*.md ``` ### 7.3 CloudAxe Desktop 1. Open **Settings → Servers** (or Add Server). 2. URL: `http://SERVER_IP:4096` or your HTTPS proxy URL. 3. Username / password: same as `OPENCODE_SERVER_*`. 4. Save when health shows online. 5. Start a session with agent **`cact-orchestrator-tech-lead`**. 6. Confirm a trivial prompt returns (proves provider auth + model routing). --- ## 8. Upgrade 1. Snapshot: ```bash TS=$(date +%Y%m%d-%H%M%S) sudo -u opencode -H bash -lc "tar -C \$HOME -czf \$HOME/cact-backup-\$TS.tgz .config/opencode .local/share/opencode" sudo cp -a /home/opencode/bin/opencode "/home/opencode/bin/opencode.prev-$TS" ``` 2. Pull / rebuild / replace the binary at the pinned ref. 3. `sudo systemctl restart opencode-cact.service` 4. Re-run section 7 probes and a desktop session. 5. Keep the previous binary and tarball until the new build is confirmed. --- ## 9. Ops checklist (copy into the ticket) - [ ] Service user created; OpenCode binary on PATH for that user - [ ] `opencode --version` recorded - [ ] `AGENTS.md` and all `cact-*.md` installed under `~/.config/opencode/` - [ ] `cact-documentation-agent` model switched off OpenRouter (or agent unused) - [ ] Provider logins present; `auth.json` mode `600` - [ ] `/etc/opencode/server.env` mode `640`, password set - [ ] `opencode-cact.service` enabled and active - [ ] Firewall / security group allowlists `4096` (or only `443` via Nginx) - [ ] `curl` health + `/api/location` succeed with basic auth - [ ] Desktop Add Server shows online - [ ] Session as `cact-orchestrator-tech-lead` succeeds - [ ] Backup tarball + previous binary path documented for rollback --- ## References in this repo - Serve command: `packages/opencode/src/cli/cmd/serve.ts` - Network flags: `packages/opencode/src/cli/network.ts` - Global path layout: `packages/core/src/global.ts` - Desktop health probes: `packages/app/src/utils/server-health.ts` - Upstream-style server docs: `packages/web/src/content/docs/server.mdx` - Container packaging of the CLI binary: `packages/opencode/Dockerfile`