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
THBOnPremProxmox 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
.5and.6. The rest of the/24is 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:8006thb2—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 installunpacks thejqdependency, 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/cachesaves fail with GNU tar exit code 2 without it, so every run misses the cache.choco install zstandard(the package id iszstandard, notzstd). - A C compiler.
go test -raceon 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.ymlcarries a Windows-only step that adds the mingw directory to the job PATH whengccis not resolvable; drop it once the services have restarted. - bash is not on the service PATH. Git for Windows is
installed, but
shell: bashsteps fail unlessC:\Program Files\Git\usr\binis on the service PATH.ci-tests.ymlavoids bash on Windows for this reason.
Linux (gh-runner-1)
- gcloud.
build.zigresolvesgcloudin local mode, which the ShellCheck script’s nestedzig build shellcheckuses. Installgoogle-cloud-clifrom Google’s apt repository. - Docker, python3, zstd and passwordless sudo for
runnerare 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 nodethb, Ubuntu 22.04.5, 2 vCPU, 16 GiB RAM, 150 GiB disk,192.168.2.249by DHCP. Built from the Ubuntu cloud image with cloud-init, matching the other personallinux-*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.ajis 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/ajis safe from it, and nothing is shared with the eightrunnerslots ongh-runner-1. - Service.
actions.runner.thehelperbees.linux-aj, actions-runner v2.337.0, in the org runner groupupgrade-deps(repository access: infrahive only), labelupgrade-deps. - Hooks.
/opt/runner-hooks/upgrade-deps-wipe.sh(installed from.github/upgrade-deps/wipe-hook.sh) as bothACTIONS_RUNNER_HOOK_JOB_STARTEDandACTIONS_RUNNER_HOOK_JOB_COMPLETEDin 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,zstdandpython3are installed. Claude Code is pinned by.github/install-claude.shinto~/.local/binon the first run. The skill’s gates also need, installed by hand on 2026-09-18:google-cloud-cli, becausebuild.zigresolvesgcloudin local mode. It is the binary only; no cloud credential is configured and the job holds none.libatomic1, for the node thatcfo --typecheckruns.- Playwright chromium’s system libraries for the docs gate, via
uv run --with playwright python -m playwright install-deps chromium. docker.iowithghrunnerin thedockergroup, for the-Ddry-run deploy-swarmctlsmoke test. Restart the runner service after adding the group.
- Token. The
HB_DEV_AJ_GITHUB_TOKENfine-grained token needs Actions read on infrahive for the-Ddry-run releasesmoke 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
THBOnPremcluster is unrelated to either Docker Swarm cluster. Those are GCP VMs inhb-infraand are described in Swarm. - Not Terraform-managed. No module, no state, no
tfoplan 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.