Build Machines
Reproducible VirtualBox images for offloading compilation to dedicated hardware. Each VM self-registers with the Custodian State Hub on boot and connects back to the development workstation via SSH reverse tunnel.
Prerequisites
- Packer >= 1.10 (
packer version) - VirtualBox >= 7.0 (
VBoxManage --version) - autossh on both workstation and VM (installed automatically in VM image)
- State Hub running on workstation (
cd ~/the-custodian/state-hub && make api)
Quick Start
1. Generate SSH keypair (one-time)
ssh-keygen -t ed25519 -f ~/.ssh/id_build -N "" -C "build-agent"
2. Build the OVA
cd infra/build-machines/haskell
packer init .
packer build .
This produces haskell-build-YYYYMMDD.ova (~4-6 GB, depending on GHC versions).
3. Import and configure
# Import the OVA
VBoxManage import haskell-build-20260420.ova
# Switch from NAT (build-time) to bridged networking
scripts/setup-vm.sh haskell-build
# Start the VM
VBoxManage startvm haskell-build --type headless
4. Inject credentials
# Prepare a directory with keys and config
mkdir -p ~/vm-keys/haskell-build
cp ~/.ssh/id_build ~/vm-keys/haskell-build/
cp ~/.ssh/id_build.pub ~/vm-keys/haskell-build/
# Edit build-agent.env from template
cp haskell/files/build-agent.env.template ~/vm-keys/haskell-build/build-agent.env
# Edit SSH_RELAY_HOST to your workstation's LAN IP
# Inject (VM must be running; uses temporary password auth)
scripts/inject-keys.sh <vm-ip> ~/vm-keys/haskell-build/
5. Install SSH config
make install-ssh-config
6. Verify
make bridge-status # check tunnel is up
ssh haskell-build # should connect via tunnel
./smoke-test.sh # full stack validation
Using the VM
# Build a Haskell project remotely
make remote-build PROJECT=~/projects/my-app
# Run tests
make remote-test PROJECT=~/projects/my-app
# Interactive GHCi
make remote-ghci PROJECT=~/projects/my-app
# Fetch build artefacts back to workstation
make fetch-artifacts PROJECT=~/projects/my-app
# Check VM info
make vm-info
Architecture
Workstation (WSL2)
├── state-hub (:8000) — sees capability entries, knows tunnel ports
└── SSH listener — accepts reverse tunnel from VM
Laptop (VirtualBox host)
└── haskell-build VM (Ubuntu 24.04, bridged)
├── GHC 9.8.4 + 9.6.6 via GHCup
├── build-agent (systemd) — registers with state-hub on boot
└── autossh: -R 12222→local:22, -L 18000→state-hub:8000
The VM connects OUT to the workstation. Two tunnels in one SSH connection:
- Reverse (
-R 12222:localhost:22): workstation can SSH into VM - Forward (
-L 18000:localhost:8000): VM can reach state-hub
Port Registry
See port-registry.yml. Range 12221-12230 supports up to 10 concurrent VMs.
Each VM must use a unique port.
Adding a GHC Version Post-Deployment
ssh haskell-build "source ~/.ghcup/env && ghcup install ghc 9.10.1"
No image rebuild required.
Troubleshooting
Tunnel not up:
- Check
journalctl -u build-agenton the VM - Verify
SSH_RELAY_HOSTin/etc/build-agent.envis reachable from the VM - Ensure the workstation's SSH server accepts the build key
Capability not in state-hub:
- Check
curl http://127.0.0.1:8000/capability-catalog/?capability_type=haskell-build-agent - The agent retries 20 times on boot; check logs for registration errors
- The forward tunnel (
-L 18000:localhost:8000) must be up before registration works
Build fails with missing libraries:
- The VM includes common Haskell build deps. For additional system libraries:
ssh haskell-build "sudo apt-get install -y libXXX-dev"
Updating the Image
Re-run Packer to build a new OVA. Import alongside the existing VM or replace it. Build artefacts and keys live on the workstation (via rsync), not in the VM — the image is disposable.