GitHub

Desktop Dev Hosts

Two Proxmox VE hosts standing in the HQ office, joined into a single cluster named THBOnPrem, on their own local VLAN. They provide on-prem virtualization capacity for desktop development work, and we refer to them collectively as the Desktop Dev Hosts.

They are the first on-prem compute this repo documents. Everything else described here runs in GCP, Azure, or Cloudflare, provisioned through Terraform and TFO. These two hosts are not Terraform-managed and have no state under infra/ — they were deployed by hand, and are administered through the Proxmox web UI and SSH.

At a Glance

Host Address Platform Cluster Managed by
thb 192.168.2.5 Proxmox VE THBOnPrem Manual (no Terraform)
thb2 192.168.2.6 Proxmox VE THBOnPrem Manual (no Terraform)
  • Cluster. Both hosts are members of the THBOnPrem Proxmox cluster, so they share configuration and are administered as one system: sign into either host’s web UI and you see and manage both.
  • Network. 192.168.2.0/24, a dedicated VLAN local to HQ, deliberately separate from the other HQ networks.
  • Reachability. From the HQ network directly, and from outside the office over the WireGuard VPN (see Access). Still no route from GCP, Azure, or Cloudflare into this subnet, and no site-to-site tunnel — cloud workloads cannot reach these hosts.
  • Guest addressing. The hosts occupy .5 and .6. The rest of the /24 is unallocated, so VM and container guests draw from the same range unless a separate scheme is introduced.

Why a Separate VLAN

Dev VMs are, by nature, the machines people install unvetted things on and point at half-finished services. Putting them on their own layer-2 segment keeps that traffic off the general HQ network and gives us a single place to write policy: the boundary between 192.168.2.0/24 and everything else. Widen access at that boundary deliberately, per port and per destination, rather than by flattening the segment.

%%{init: {'theme':'dark','themeVariables':{'fontSize':'16px','lineColor':'#9ca3af'},'flowchart':{'curve':'basis','nodeSpacing':45,'rankSpacing':55}}}%%
flowchart TB
  REMOTE["Remote users
off-site"]:::ext WG["WireGuard VPN
Ubiquiti gear"]:::net HQ["Rest of HQ network"]:::ext subgraph HQVLAN["HQ Desktop Dev VLAN  ·  192.168.2.0/24"] direction LR H1["thb
192.168.2.5"]:::host H2["thb2
192.168.2.6"]:::host H1 ---|"THBOnPrem cluster"| H2 end G["VM / container guests
same /24, unallocated"]:::guest CLOUD["GCP  ·  Azure  ·  Cloudflare"]:::ext REMOTE -->|"tunnel in"| WG WG --> HQVLAN HQ -->|"VLAN boundary
policy enforced here"| HQVLAN HQVLAN -.->|"hosts"| G CLOUD -.->|"no route"| HQVLAN classDef host fill:#bfdbfe,stroke:#1d4ed8,stroke-width:1px,color:#0b1e3b classDef guest fill:#bbf7d0,stroke:#15803d,stroke-width:1px,color:#06281a classDef net fill:#fde68a,stroke:#b45309,stroke-width:1px,color:#3a2400 classDef ext fill:#e5e7eb,stroke:#4b5563,stroke-width:1px,color:#111827

Access

Both hosts serve the Proxmox management interface on its standard port, 8006, over HTTPS. Because they are clustered, either address administers the whole of THBOnPrem:

  • thb — https://192.168.2.5:8006
  • thb2 — https://192.168.2.6:8006

SSH is available on each host on the usual port 22. Certificates are Proxmox’s self-signed defaults until we put a real one in place, so expect a browser warning on first connection.

Getting onto the network

  • At HQ. Reachable directly, subject to policy at the VLAN boundary.
  • Off-site. We run a WireGuard VPN terminated on our Ubiquiti equipment that puts remote users onto the on-prem network. Connect to it and these hosts are reachable as if you were in the office — no separate tunnel or jump host required.

This is our own VPN path, not a cloud one. Neither IAP (GCP) nor Bastion (Azure) applies to these hosts; those reach cloud VMs only.

GitHub Actions Runners

Two guests host the org-level GitHub Actions runners that infrahive CI (.github/workflows/ci-tests.yml) runs on. Both are persistent runners: the workspace is cleaned by actions/checkout every run, but per-user caches (zig global cache, Go build and module caches, Playwright browsers) survive between runs and are shared by all eight runner slots on a box. That is where the speed over GitHub-hosted runners comes from.

Guest Host Address Runners Labels
gh-runner-1 (Ubuntu 22.04, 12 vCPU, 122 GiB) thb2 192.168.2.182 gh-runner-1..8, user runner self-hosted, linux, x64, thb2
win-dev-1 (Windows Server 2022, 12 vCPU, 128 GiB) thb 192.168.2.38 win-dev-1-1..8, service account ghrunner self-hosted, windows, x64, thb
linux-aj (Ubuntu 22.04, 2 vCPU, 16 GiB) thb 192.168.2.249 linux-aj, user ghrunner self-hosted, linux, x64, upgrade-deps

Prerequisites the runners must satisfy

These are the Setup prerequisites for a developer machine, applied to the runner guests. All were applied on 2026-09-11; a rebuilt or additional runner needs the same.

Windows (win-dev-1)

  • Developer Mode. zig build install unpacks the jq dependency, which contains a symlink; Windows refuses symlink creation without it. HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\AllowDevelopmentWithoutDevLicense = 1.
  • Git symlinks. git config --system core.symlinks true.
  • zstd. actions/cache saves fail with GNU tar exit code 2 without it, so every run misses the cache. choco install zstandard (the package id is zstandard, not zstd).
  • A C compiler. go test -race on windows/amd64 needs cgo. choco install mingw.
  • Restart the runner services after PATH changes. The services capture PATH at startup; Chocolatey’s PATH additions (mingw, zstd) are invisible to jobs until Restart-Service actions.runner.*. ci-tests.yml carries a Windows-only step that adds the mingw directory to the job PATH when gcc is not resolvable; drop it once the services have restarted.
  • bash is not on the service PATH. Git for Windows is installed, but shell: bash steps fail unless C:\Program Files\Git\usr\bin is on the service PATH. ci-tests.yml avoids bash on Windows for this reason.

Linux (gh-runner-1)

  • gcloud. build.zig resolves gcloud in local mode, which the ShellCheck script’s nested zig build shellcheck uses. Install google-cloud-cli from Google’s apt repository.
  • Docker, python3, zstd and passwordless sudo for runner are already present. Go, uv, Zig, terraform, shellcheck and pandoc are downloaded by the build system into the zig global cache.

The eight-slot runner accounts are privileged (local Administrator on Windows, passwordless sudo on Linux) and those services are in the org’s default runner group, so any repository in the org can schedule jobs onto them. Anything that runs in CI on this branch of infrahive runs with those privileges on the HQ dev VLAN.

Dedicated runner for upgrade-deps

.github/workflows/upgrade-deps.yml runs Claude Code with a GitHub token and a Claude token in its environment. It gets a whole guest to itself, rather than a slot on gh-runner-1, so no other job shares its uid or home directory while it runs. Applied 2026-09-18.

  • Guest. linux-aj, VMID 109 on node thb, Ubuntu 22.04.5, 2 vCPU, 16 GiB RAM, 150 GiB disk, 192.168.2.249 by DHCP. Built from the Ubuntu cloud image with cloud-init, matching the other personal linux-* guests.
  • Users. The runner service runs as ghrunner, which exists only for that purpose and has passwordless sudo so the docs gate can install Playwright’s system dependencies. aj is the human login, reachable with AJ’s GitHub SSH key. They are separate on purpose: the wipe hook clears the runner user’s home before and after every job, so anything kept in /home/aj is safe from it, and nothing is shared with the eight runner slots on gh-runner-1.
  • Service. actions.runner.thehelperbees.linux-aj, actions-runner v2.337.0, in the org runner group upgrade-deps (repository access: infrahive only), label upgrade-deps.
  • Hooks. /opt/runner-hooks/upgrade-deps-wipe.sh (installed from .github/upgrade-deps/wipe-hook.sh) as both ACTIONS_RUNNER_HOOK_JOB_STARTED and ACTIONS_RUNNER_HOOK_JOB_COMPLETED in the service’s .env. The verify step fails the job if the installed copy drifts from the one in the repository.
  • Prerequisites. qemu-guest-agent, curl, git, jq, unzip, zstd and python3 are installed. Claude Code is pinned by .github/install-claude.sh into ~/.local/bin on the first run. The skill’s gates also need, installed by hand on 2026-09-18:
    • google-cloud-cli, because build.zig resolves gcloud in local mode. It is the binary only; no cloud credential is configured and the job holds none.
    • libatomic1, for the node that cfo --typecheck runs.
    • Playwright chromium’s system libraries for the docs gate, via uv run --with playwright python -m playwright install-deps chromium.
    • docker.io with ghrunner in the docker group, for the -Ddry-run deploy-swarmctl smoke test. Restart the runner service after adding the group.
  • Token. The HB_DEV_AJ_GITHUB_TOKEN fine-grained token needs Actions read on infrahive for the -Ddry-run release smoke test (gh workflow view), on top of what opening pull requests needs.

What These Are Not

Worth stating plainly, because the names invite confusion:

  • Not swarm nodes. The THBOnPrem cluster is unrelated to either Docker Swarm cluster. Those are GCP VMs in hb-infra and are described in Swarm.
  • Not Terraform-managed. No module, no state, no tfo plan covers them. Changes here are made by hand and are not captured in this repo’s plan output.
  • Not the same thing as the GCP Dev Windows VM. For a disposable, toolchain-baked Windows box to build and debug HealthAlign PMS apps, use Dev Windows VM — it is cloud-hosted, reachable over IAP from anywhere, and reaped automatically. The Desktop Dev Hosts are durable on-prem capacity with no expiry or reaper.
Edit this page