Skip to content
Talk to our solutions team

Prepare a build machine

A bare Linux host that can build every artefact your product needs — Go services, Node and frontend bundles, Rust and Zig components, and anything linking native image libraries — installed by a flow rather than by hand.

When you finish you will have one flow that takes a fresh host to a working builder, and a final task that fails if any toolchain is missing. Run it against the next machine and you get the same machine, not a similar one.

Installing a toolchain by hand takes twenty minutes and works. Doing it by hand four times produces four machines that are almost the same, and “almost” is where the cost lands.

Without thisWith this
A new builder takes a day, and the person who set up the last one is on leaveA new builder takes one command and the time to download
Two machines disagree about a compiler minor, so a build passes on one and fails on the otherEvery machine pins the same versions from the same file
Upgrading Go means finding every host and remembering what you didChange one var, re-run, done
”It works on the build box” is unfalsifiableThe box is a flow you can read, diff and review
A lost builder is an outageA lost builder is a re-run

The version list at the top of this flow becomes the definition of your build environment. When a build breaks after an upgrade, the diff that caused it is one line in version control rather than a memory.

This pays for itself the second time you run it, and every time someone joins.

You needWhy
A host you can reach over SSHEvery task runs remotely
A user with sudoToolchains install system-wide
The kis CLI locallyTo run the flow

Step 1 — declare what “a build machine” means

Section titled “Step 1 — declare what “a build machine” means”

Start with the versions, not the commands. A build machine is defined by the versions it pins, and putting them at the top means upgrading one is a one-line diff rather than a search.

name: prepare-build-machine
vars:
host: builder.example.com
user: build
keypath: /path/to/key
go_version: "1.23.4"
node_version: "22"
rust_channel: stable
zig_version: "0.13.0"

Nothing installs yet. This is the contract.

Step 2 — install the system compiler first

Section titled “Step 2 — install the system compiler first”

Everything else depends on it. Native modules, image libraries and Rust’s linker all need a C toolchain present before they are installed, so this task comes first and the rest follow.

tasks:
- name: system-compiler
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
sudo apt-get update -y
sudo apt-get install -y build-essential pkg-config curl git

Each is its own task. That matters: when one fails you see which, and you can re-run the flow without repeating the ones that succeeded.

- name: go
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl -fsSL https://go.dev/dl/go{{go_version}}.linux-amd64.tar.gz -o /tmp/go.tgz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tgz
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
- name: node
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl -fsSL https://deb.nodesource.com/setup_{{node_version}}.x | sudo -E bash -
sudo apt-get install -y nodejs
- name: rust
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain {{rust_channel}}

The ones that only announce themselves when a build fails at link time. Install them explicitly rather than letting a package pull them in, so the machine is complete before any build runs.

- name: native-libs
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
sudo apt-get install -y libvips-dev libssl-dev zlib1g-dev

Step 5 — verify, and fail if anything is missing

Section titled “Step 5 — verify, and fail if anything is missing”

This is the task that makes the flow worth having. Without it, “the flow ran” and “the machine works” are different claims and you find out which during a build.

- name: verify
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
/usr/local/go/bin/go version
node --version
~/.cargo/bin/rustc --version
pkg-config --exists vips && echo "vips ok"
echo "BUILD MACHINE READY"

set -e is what turns this from a report into a gate: the first missing tool exits non-zero and the flow fails.

name: prepare-build-machine
list: true
vars:
host: builder.example.com
user: build
keypath: /path/to/key
go_version: "1.23.4"
node_version: "22"
rust_channel: stable
tasks:
- name: system-compiler
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
sudo apt-get update -y
sudo apt-get install -y build-essential pkg-config curl git
- name: go
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl -fsSL https://go.dev/dl/go{{go_version}}.linux-amd64.tar.gz -o /tmp/go.tgz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tgz
echo 'export PATH=$PATH:/usr/local/go/bin' | sudo tee /etc/profile.d/go.sh
- name: node
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl -fsSL https://deb.nodesource.com/setup_{{node_version}}.x | sudo -E bash -
sudo apt-get install -y nodejs
- name: rust
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain {{rust_channel}}
- name: native-libs
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
sudo apt-get install -y libvips-dev libssl-dev zlib1g-dev
- name: verify
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
/usr/local/go/bin/go version
node --version
~/.cargo/bin/rustc --version
pkg-config --exists vips && echo "vips ok"
echo "BUILD MACHINE READY"

Prefer the flow for more than one machine. Each toolchain is its own node, so a failure names the toolchain, and re-running skips what already succeeded. The script’s single connection is faster for one host and loses that.

Run it:

Terminal window
kis flow -f prepare-build-machine.yaml

Check what it would do before touching a host:

Terminal window
kis flow -f prepare-build-machine.yaml --dryrun

Re-run one task after fixing something, instead of the whole flow:

Terminal window
kis flow -f prepare-build-machine.yaml -t rust

The flow verifies itself — the last task prints BUILD MACHINE READY only when every check passed. If you want the machine to prove it independently, build something small on it before trusting it with a release.

ChangeWhere
A different distributionThe apt-get lines. The structure holds; only the package manager changes
More or fewer toolchainsAdd or remove a task. Keep one toolchain per task
Pinned Node minornode_version, and switch the NodeSource line for a tarball install
Several machines at onceSee Run a flow across many hosts
Per-environment valuesKeep them in an environment file and pass -e env.yaml -n staging