Run Beachball with Firecracker
In this tutorial, we start the local scheduler, enroll a real Firecracker host, boot Ubuntu 24.04, send a shell payload to the guest, and release the allocation. Direct WireGuard tests do not require a packet relay.
Use a disposable x86_64 Linux host with KVM. The agent runs as root and manages cgroups, network namespaces, TAP devices, nftables rules, mounts, and Firecracker processes. Do not run this tutorial on a host whose network or workloads you cannot disrupt.
Run every command from the repository root.
Run the setup script
To prepare the complete tutorial host, run:
pnpm setup:firecrackerThe script runs pnpm reset, installs the Ubuntu host tools and development
dependencies, prepares the signed Ubuntu image, and enrolls and checks
host-1. Agent setup creates the cgroup and network configuration that
Firecracker needs. The image download and build can take several minutes on the
first run.
The script starts the scheduler only while it enrolls the host and stops it before it exits, then prints the commands that start the scheduler, start the agent, and submit the Ubuntu job. Use the remaining sections when you want to run or inspect each setup step yourself.
Install the host tools
On Ubuntu 24.04, install the build and runtime tools:
sudo apt-get update
sudo apt-get install -y \
binutils build-essential curl e2fsprogs iproute2 nftables openssl \
wireguard-tools xz-utilsAfter setup creates the agent configuration, bb agent doctor checks the host
and reports remediation for every failed requirement.
Prepare the checkout and Ubuntu image
Enter the checked-in development environment and install the repository dependencies:
direnv allow
pnpm setupCreate a local Ed25519 signing seed. Keep an existing seed because its public key identifies the trusted development image signer.
install -d -m 0700 .dev/beachball
signing_key="$PWD/.dev/beachball/image-signing.key"
if [ ! -s "$signing_key" ]; then
openssl rand -out "$signing_key" 32
fi
chmod 0600 "$signing_key"Build the static guest once so the image preparation step can verify the exact binary that it injects:
RUSTFLAGS='-C target-feature=+crt-static' cargo build \
--locked \
--release \
--target x86_64-unknown-linux-gnu \
--manifest-path agent/guest/Cargo.toml
guest_binary="$PWD/agent/target/x86_64-unknown-linux-gnu/release/beachball-guest"
export BEACHBALL_EXPECTED_GUEST_SHA256="$(sha256sum "$guest_binary" | cut -d' ' -f1)"
export BEACHBALL_IMAGE_SIGNING_KEY_FILE="$signing_key"Download the pinned Firecracker 1.17.0 release, the supported kernel, and the Ubuntu 24.04 root filesystem. The script verifies each download, injects beachball-guest, builds an ext4 image, and signs its manifest.
assets="$PWD/.dev/beachball/ubuntu-assets"
./agent/scripts/prepare-ubuntu-24.04.sh "$assets"
source "$assets/runtime.env"Any OCI container image can be booted the same way. Once the assets above exist, agent/scripts/boot-oci-image.sh alpine:3.20 (or ubuntu:24.04, python:3.12-alpine, ...) builds a signed image with bb image build and boots it through the placement driver; see Boot OCI images.
This step downloads and builds several gigabytes of data. Later runs reuse the verified files under .dev/beachball/ubuntu-assets.
Prepare and start the scheduler
Create the local credentials, scheduler secrets, and deployment manifest. WORKLOAD_IMAGE_REF places the signed Ubuntu image in the local workload class.
source .dev/beachball/ubuntu-assets/runtime.env
WORKLOAD_IMAGE_REF="$BEACHBALL_IMAGE_REF" \
BEACHBALL_IMAGE_MANIFEST_BASE_URL="$BEACHBALL_IMAGE_MANIFEST_BASE_URL" \
BEACHBALL_IMAGE_TRUSTED_PUBLIC_KEYS="$BEACHBALL_IMAGE_TRUSTED_PUBLIC_KEYS" \
pnpm dev:prepare
source .dev/beachball/environment.shThe preparation command keeps an existing root secret and creates it only
when it is missing. The development manifest makes the signed Ubuntu image the
default class image and publishes the image settings that hosts read when
they join.
In the first terminal, start the scheduler:
pnpm devKeep this process open. The public API listens on http://127.0.0.1:8787.
In a second terminal, load the environment and confirm that the API is live:
source .dev/beachball/environment.sh
bb health liveYou should see live: ok.
Join the host
Create a one-use join token. The token names the host and expires after an hour:
join_token="$(bb --profile local-operator host enrollment-token create host-1 --token-output -)"Without --token-output, the command prints a ready sudo bb agent join
command instead, which is what you would paste on a separate machine.
Build bb and join. Every setup value that the scheduler knows (region,
network prefix, connection verification key, image settings) comes from
/v1/.well-known/beachball, and the host's capacity, uplink interface, and
WireGuard port are detected. The flags below only keep this checkout's state
out of the system paths:
host_state="$PWD/.dev/beachball-host"
config="$host_state/agent.toml"
jailer_root="/var/bbj-$(printf '%s' "$PWD" | sha256sum | cut -c1-8)"
cargo build --locked --release --manifest-path agent/Cargo.toml
sudo "$PWD/agent/target/release/bb" agent --config "$config" \
join "http://127.0.0.1:8787#$join_token" \
--runtime-manifest "$PWD/.dev/beachball/ubuntu-assets/runtime-manifest.json" \
--service-user root \
--agent-credential-file "$host_state/agent.token" \
--state-root "$host_state/state" \
--runtime-root "$host_state/run" \
--jailer-chroot-base-dir "$jailer_root"join validates the runtime asset digests, enables the cgroup controllers,
enrolls the host, and runs every host check, printing a fix for each failure.
The host is active as soon as it joins; the scheduler places work on it after
its first healthy heartbeat.
Start the Firecracker host
Start the agent and keep the command open:
sudo "$PWD/agent/target/release/bb" agent --config "$config" runAfter a host reboot, run setup again with the same flags before run (the
join token is no longer needed): it recreates the ephemeral cgroup parent,
keeps the host credential, and does not enroll twice.
In a third terminal, check the host:
source .dev/beachball/environment.sh
bb --profile local-operator host get host-1The host view reports active, and healthy once the first heartbeat
arrives.
Check direct connectivity
The agent publishes its WireGuard public key and local UDP candidate through its normal control session. A client creates a Connection object only when you run bb proxy. The host then applies the signed connection grant and routes only that client's /128 through bbconn0.
For relay fallback tests, create TLS files and copy
deploy/relay.example.toml to a temporary
configuration. Start the unprivileged relay in another terminal:
agent/target/release/bb relay --config /absolute/path/to/relay.toml runStop the relay to prove that the direct path remains independent.
Start an Ubuntu VM for SSH
After the scheduler and the agent are running and host-1 is active, run:
pnpm run:ubuntuThe script creates or reuses a private Network, creates an Ubuntu allocation,
and installs a local SSH public key through the one-time bootstrap payload. It
then prints the bb proxy, ssh, and allocation release commands. The script
does not keep a proxy or SSH process open.
Run a command in Ubuntu
bb run creates an allocation, waits for the guest readiness handshake, and
delivers the command as a shell payload. The default class already names the
signed Ubuntu image, so no image argument is needed:
bb run --max-lifetime 30m -- sh -c '. /etc/os-release; echo "hello from $PRETTY_NAME"; uname -a'The command prints the allocation, including its allocationId; keep it for
the steps below. The ready state means Firecracker booted the signed image
and the scheduler completed both guest authentication handshakes. Pass
--cpu-millis, --memory-mib, or --disk-mib to size the VM, or
--script FILE to deliver a script instead of a command.
The same steps are available individually: bb allocation create --wait ready
followed by bb allocation bootstrap-payload put.
The Ubuntu fixture executes text/x-shellscript payloads with /bin/sh. Read the host-local serial console before releasing the allocation:
sudo find "$PWD/.dev/beachball-host" \
-path '*/logs/console.log' \
-type f \
-exec tail -n 60 {} \;Look for BEACHBALL_JOB_START, the Ubuntu greeting, and BEACHBALL_JOB_EXIT.
Measure allocation latency
scripts/measure-allocation-latency.mjs creates allocations one at a time, waits for each to become ready, and releases it before the next. It prints where the time went three ways: the client's wall clock, the scheduler's allocation events, and the agent's own steps, which it reads from the agent's log:
node scripts/measure-allocation-latency.mjs --runs 5 --agent-log .dev/logs/agent.log
node scripts/measure-allocation-latency.mjs --class surf-linux-x64-medium --profile surf-local --jsonThe first allocation of an image on a host also fetches it and fills the host's shared artifact copies, and the first of an image and size builds its boot snapshot; later runs show the steady state.
Inspect and release the workload
The job API presents the same workload and uses the allocation ID:
bb job list
bb job get "$allocation_id"
bb allocation get "$allocation_id"Release the allocation and wait for the agent to stop Firecracker and remove its resources:
bb allocation release "$allocation_id" --waitStop the relay, agent, and scheduler with Ctrl+C.
To discard the local celld state and start with new credentials, run:
pnpm resetThe reset command stops Beachball processes from this checkout. It removes the
scheduler state (scheduler/.celld/dev, or the Docker Compose object-store
volume in s3 mode), generated scheduler files, and enrolled host state.
It preserves the downloaded Ubuntu assets and .dev/beachball/image-signing.key.
Run pnpm reset -- --dry-run to inspect the deletion scope.
The scheduler state, signed image, and enrolled agent state remain in place.
The next run can start with pnpm dev and the agent run command. If you run pnpm dev:prepare again,
pass WORKLOAD_IMAGE_REF="$BEACHBALL_IMAGE_REF" again so the manifest keeps the
signed Ubuntu image. Use pnpm reset only when you intend to delete the local
celld state and enroll the host again.
This tutorial allows file:// image artifacts on one local host. Production hosts must load signed manifests and image artifacts over HTTPS and must omit --allow-file-images.