Build once, fork many¶
Forking lets you prepare a source VM once and start many independent VMs from it, without repeating the setup before every job. Slicer supports two ways to fork:
- Cold forking commits the disk of a stopped VM. Each fork boots from that disk state.
- Hot forking commits the memory and disk of a running VM. Each fork resumes the captured processes.
Both use the same commit and fork workflow, so you can fork as many clean VMs as you need from a single prepared source.
Prefer cold forking
Use cold forking where possible. It supports more configurations and lets you adjust CPU and RAM for each fork. Use hot forking when process start-up takes enough time to justify capturing memory.
Cold fork or hot fork?¶
Cold forking commits the disk of a stopped VM and boots each fork from that state. It works across more configurations, can scale CPU and RAM per fork, and suits jobs where the preparation is stored on disk, such as compilers, dependencies, and checked-out source trees.
Hot forking commits a running VM and resumes its processes. It is the better fit when the cost is in-process start-up: a browser that must initialise before it can work, a model that must be loaded into memory, or a runtime with a long warm-up. Each fork skips the guest boot and process initialisation and starts at the exact point where the source was captured.
Cold forking¶
Cold forking starts new microVMs from the disk state of a stopped, persistent VM.
Use it when the VM needs an expensive setup step before each job: installing a compiler, downloading dependencies, pulling container images, or preparing an agent. Set up one builder, commit its disk, then fork as many clean VMs as you need.
Only disk state is committed. Each forked VM boots normally with its own hostname, machine ID, and network identity by default.
Requirements¶
On Linux, cold forking requires:
- Firecracker
image,devmapper, orzfsstorage- Bridge or isolated networking
- A stopped, persistent source VM
Firewall overrides on slicer vm fork, including --allow, --no-allow, and --drop, require Linux isolated networking and are unavailable on macOS. Both platforms support --network none for a fork with no virtual NIC.
The CLI creates persistent forked VMs by default. Pass --rm for an ephemeral fork whose cloned storage is discarded when the VM stops or is deleted.
Create an empty host group¶
This example uses the min image for the fastest boot, image storage for the simplest setup, and isolated networking so the fork can have its egress removed:
slicer new runners \
--count 0 \
--min \
--storage image \
--net isolated \
> slicer.yaml
Start Slicer:
sudo slicer up slicer.yaml
The generated isolated host group allows egress by default. The builder can download packages and dependencies before its disk is committed.
Prepare the builder¶
In another terminal, create a persistent VM:
BUILDER=$(slicer vm add runners \
--persistent \
--tag purpose=cold-fork-builder \
--wait \
--timeout 10m \
--json | jq -r '.hostname')
echo "$BUILDER"
The response contains the allocated hostname. Install and prepare everything that should be present in each forked VM. For example:
slicer vm exec "$BUILDER" -- \
"sudo arkade system install go"
slicer vm exec "$BUILDER" -- \
"git clone https://github.com/alexellis/arkade \
/home/ubuntu/arkade && \
cd /home/ubuntu/arkade && \
/usr/local/go/bin/go build -mod=vendor -o ./arkade"
Do not store reusable credentials in the builder. Every fork inherits its disk.
Commit the disk¶
Shut down the builder, then commit it:
slicer vm shutdown "$BUILDER"
# The Firecracker shutdown request returns after the VM process has stopped.
COMMIT=$(slicer vm commit "$BUILDER" \
--tag arkade \
--label toolchain=go \
--cache-key arkade-go-v1 \
--quiet)
echo "$COMMIT"
The commit ID identifies an immutable disk parent, for example:
cmt-runnersx1-a1b2c3d4e5f6a7b8
The source VM remains stopped. A committed source cannot be committed again with different metadata. Create another persistent builder when you need a different base.
Fork a VM¶
To inherit the host group's existing network policy:
RUNNER=$(slicer vm fork "$COMMIT" --wait --tag role=runner --quiet)
On Linux with isolated networking, clear the inherited allow list and drop everything else for a fork with no egress. On macOS, use an offline fork instead:
JOB=arkade-run-$(cat /proc/sys/kernel/random/uuid)
RUNNER=$(slicer vm fork "$COMMIT" \
--wait \
--tag role=runner \
--tag "job=$JOB" \
--no-allow \
--drop 0.0.0.0/0 \
--quiet)
echo "$RUNNER"
--drop 0.0.0.0/0 on its own is not enough when the host group has the default --allow 0.0.0.0/0 rule. The allow rule takes precedence, so use --no-allow to clear it.
The DROP is enforced on the Slicer host, outside the guest. A root process inside the VM cannot remove it with iptables, ignore it by opening a raw socket, or bypass it by unsetting a proxy variable.
The CLI waits for guest-agent readiness and identity finalisation by default. Use --no-wait to return after the child is accepted. The examples pass --wait explicitly. If the client disconnects, the daemon continues the fork. Recover the allocated hostname through the unique job tag, then describe it:
RUNNER=$(slicer vm list --json | jq -r --arg tag "job=$JOB" \
'.[] | select((.tags // []) | index($tag)) | .hostname' | head -n1)
slicer vm describe "$RUNNER"
Prepare online, fork offline¶
After installing dependencies and committing the builder's disk as above, launch an offline fork:
RUNNER=$(slicer vm fork "$COMMIT" --network none --wait --quiet)
slicer vm exec "$RUNNER" -- /home/ubuntu/arkade/arkade --help
The fork retains the installed dependencies but has no NIC or IP lease. Exec, shell, file copy, and forwarding to guest loopback services continue to work over vsock. This workflow also works with disk commits on macOS.
On Linux and macOS, pass --network none for every offline fork. Omitting the flag uses the host group's networking and creates a fork with networking enabled again.
Offline forks require disk commits. Hot forking does not support offline forks.
Control fork behaviour¶
By default, Slicer fixes the hostname, machine ID, and SSH host keys. If sshd is installed, Slicer follows the image's configured HostKey paths. Otherwise, it skips the SSH host-key work.
For maximum throughput when duplicated guest identity is acceptable, skip every fix-up and do not wait:
RUNNER=$(slicer vm fork "$COMMIT" \
--no-fixups \
--no-wait \
--rm \
--quiet)
You can also select individual fix-ups, scale CPU and RAM down within the source host group's limits, replace inherited tags, and replace or clear inherited secret grants:
RUNNER=$(slicer vm fork "$COMMIT" \
--wait \
--fixup hostname \
--fixup machine-id \
--cpu 1 \
--ram-mb 512 \
--replace-tags \
--tag role=runner \
--no-secrets \
--rm \
--quiet)
--no-secrets also removes inherited secret files during finalisation. Omitting tag, secret, CPU, or RAM options inherits the committed VM's launch configuration. On Linux, firewall overrides are inherited for online forks. Userdata is disk state and is not run again on a fork.
Use and delete the VM¶
The forked VM inherits the compiler, source tree, vendored dependencies, and Go build cache from the builder. Change one line and rebuild:
slicer vm exec "$RUNNER" -- \
"cd /home/ubuntu/arkade && \
sed -i \
's/boot Linux microVMs instantly/boot Linux microVMs quickly/' \
pkg/thanks.go && \
/usr/local/go/bin/go build -mod=vendor -o ./arkade"
Delete the VM when the job is complete:
slicer vm delete "$RUNNER"
The committed parent remains available for another fork.
Reuse the builder step¶
A cache key lets an API-driven workflow skip the whole builder step on its next run. It is similar to a Docker build cache, but Slicer caches the complete committed disk rather than individual layers.
Look up the key before launching a builder:
KEY=arkade-go-v1
COMMIT=$(slicer vm commit list \
--cache-key "$KEY" \
--json | jq -r '.[0].commit_id // empty')
if [ -z "$COMMIT" ]; then
BUILDER=$(slicer vm add runners \
--persistent \
--tag purpose=cold-fork-builder \
--wait \
--json | jq -r '.hostname')
# Run the preparation commands from above.
slicer vm shutdown "$BUILDER"
COMMIT=$(slicer vm commit "$BUILDER" \
--cache-key "$KEY" \
--quiet)
fi
RUNNER=$(slicer vm fork "$COMMIT" --wait --tag role=runner --quiet)
The caller owns the cache key. Include the inputs which make the builder result reusable, such as the toolchain version, dependency lock-file digest, or setup-script version. Change the key to invalidate the cached result.
The same lookup is available through GET /vm/commits?cache_key=..., ListCommits() in the Go SDK, and client.commits.list() in the TypeScript SDK.
Organise committed parents¶
Add tags, labels, or a deterministic cache key when the calling application needs to find a committed parent later. Commit metadata is immutable, so set it on the initial slicer vm commit command, as shown above.
List all committed parents, or filter the list:
slicer vm commit list
slicer vm commit list --tag arkade
slicer vm commit list --cache-key arkade-go-v1
slicer vm commit list --source "$BUILDER"
Delete a committed parent when neither its source VM nor any forked VM uses it:
slicer vm commit delete "$COMMIT"
Cold fork, suspend, and restore¶
Cold fork and suspend solve different problems:
slicer vm commitrecords disk state from a stopped VM. Each fork is a new VM which boots from that state.slicer vm suspendrecords memory, disk, and device state.slicer vm restoreresumes the same VM later.
Persistent Firecracker VMs in Slicer, and VMs in Slicer for Mac, can suspend and restore today. A suspended VM cannot yet be forked into several VMs. To fork a running VM and resume its processes instead of booting from disk, see hot forking.
Cold fork or a custom image?¶
A custom Slicer image is the better fit for a versioned base which needs to be distributed across several hosts. It can be built in CI, pushed to a registry, and referenced from a host group.
Cold forking lets you prepare a VM interactively, commit it, then fork locally without building a Dockerfile, pushing and pulling an OCI image, or unpacking it again on the Slicer host.
Hot forking¶
Hot forking starts new microVMs from the captured memory and disk state of a running VM. Each fork resumes the captured processes. Only the source VM is captured. Every fork is an independent VM with its own hostname, machine ID, network identity, and private writable disk.
Linux only
Hot forking is available on Linux only. Slicer for Mac supports disk-based cold forks only.
Requirements¶
- Firecracker on Linux amd64
imagestorage- Isolated networking
- A running, persistent source VM
- A ready
slicer-agentversion 0.1.238 or later - The source VM running on the same CPU and RAM shape you want for the forks
Memory forks cannot change the CPU or RAM shape of the captured VM. Slicer for Mac supports disk-based cold forks only.
Browser automation use case¶
For example, services that take screenshots, scrape web pages, or automate browser interactions can use hot forking to avoid repeating browser start-up for every job. Install a browser in a source VM and start it on a blank page. Leave it running while it waits for a job, then capture the VM's memory and disk.
Each fork resumes an independent copy of the browser, already running and ready to navigate. The guest boot, runtime start-up, and browser initialisation happen once, during preparation.
Each child then receives its own URL and task, such as taking a screenshot, scraping page content, or interacting with forms and links. Changes in one child do not affect the source or other forks.
The following commands show the VM lifecycle. Browser setup and job delivery depend on your application.
Configure a host group¶
Generate a configuration with image storage and isolated networking, then start Slicer:
slicer new browsers \
--count 0 \
--cpu 2 \
--ram 2 \
--storage image \
--net isolated \
> browsers.yaml
sudo -E slicer up browsers.yaml
Prepare the source VM¶
In another terminal, create a persistent VM and open a shell:
SOURCE=$(slicer vm add browsers \
--persistent \
--wait \
--json | jq -r '.hostname')
slicer vm shell "$SOURCE"
Prepare the workload you want each fork to resume. For example, install a browser and its dependencies, start it on a blank page, and have it wait for a job. Run it as a service or background process so it stays running when you exit the shell.
Each fork inherits process state, background jobs, and credentials.
Capture its memory¶
Keep the source running and create a memory commit. Slicer briefly pauses the VM, captures RAM and disk, then resumes it:
COMMIT=$(slicer vm commit "$SOURCE" \
--memory \
--tag browser-ready \
--cache-key browser-v1 \
--quiet)
The --memory flag is required to create a hot (memory) commit.
Fork and use a VM¶
Fork the commit to resume the prepared browser in a new VM:
RUNNER=$(slicer vm fork "$COMMIT" --wait --rm --quiet)
slicer vm shell "$RUNNER"
Slicer assigns a new hostname, machine ID, and SSH host keys. These updates are mandatory, and the child stays in network quarantine until they finish. Forking waits by default. Use --no-wait to return earlier.
Forks inherit the source's launch configuration. You can replace or clear tags and secret grants. --rm discards the child's disk when it stops. CPU and RAM allocations can not be changed.
At this point, your prepared workload is running in the fork and ready for a job.
When finished, exit the shell and delete the child:
slicer vm delete "$RUNNER"
The commit remains available for further forks.
See Organise committed parents for how to list, filter, and delete memory commits.
You can automate this entire workflow with the Go SDK, from preparing the source and creating commits to running jobs in forks and cleaning up.