Files
the-custodian/infra/build-machines

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-agent on the VM
  • Verify SSH_RELAY_HOST in /etc/build-agent.env is 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.