> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Golden Images

> Build project-specific VM snapshots for instant agent environments

A golden image is a committed VM snapshot with your tools, dependencies, and configuration pre-installed. Boot from it to get a working environment in seconds instead of spending minutes on setup every time.

## When You Need a Custom Golden Image

Use the stock `default` rootfs when your project has simple needs (standard Ubuntu packages, small dependency trees). Build a custom golden when:

* **Installation takes too long**: `npm install`, `pip install`, or `cargo build` takes more than a few minutes on a fresh VM
* **System dependencies are required**: Your project needs `apt install` packages not in the base Ubuntu image
* **Version pinning matters**: You need exact versions of Node, Python, Rust, or system libraries
* **Build toolchains are heavy**: Custom compilers, cross-compilation tools, CUDA/ROCm
* **Caches improve performance**: Pre-populated package manager caches, pre-built indexes, database dumps
* **Agent tooling is needed**: pi, Claude Code, or other agents pre-installed with the RPC scaffold ready

## The Build Process

### Step 1: Start a Builder VM

Use generous resources for the build. You'll commit a lean snapshot at the end.

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
mkdir golden-build && cd golden-build
vers init
```

Edit `vers.toml` for build resources:

```toml theme={"theme":{"light":"min-light","dark":"min-dark"}}
[machine]
mem_size_mib = 8192        # 8 GB for builds
vcpu_count = 4
fs_size_vm_mib = 30720     # 30 GB to fit toolchains + caches

[rootfs]
name = "default"

[kernel]
name = "default.bin"
```

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run -N golden-builder
vers connect golden-builder
```

### Step 2: Install Everything

Inside the VM, set up DNS and install your stack:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
apt-get update && apt-get install -y \
  build-essential git curl wget tmux \
  ca-certificates gnupg
```

#### Example: Node.js Agent Environment

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# Node.js 22
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs

# Agent tooling
npm install -g @mariozechner/pi-coding-agent
npm install -g @anthropic-ai/claude-code

# Project dependencies (clone and install, then remove source)
git clone https://github.com/your-org/your-project /tmp/project-deps
cd /tmp/project-deps && npm install
# Keep node_modules warm in npm cache, remove source
npm cache add .
cd / && rm -rf /tmp/project-deps
```

#### Example: Python ML Environment

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
apt-get install -y python3 python3-pip python3-venv
python3 -m pip install --upgrade pip
python3 -m pip install torch transformers fastapi uvicorn
```

#### Example: Haskell Build Environment

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# Install nix (single-user, no daemon)
groupadd -f nixbld
for i in $(seq 1 10); do
  useradd -g nixbld -G nixbld -d /var/empty -s /sbin/nologin nixbld$i 2>/dev/null || true
done
mkdir -p /nix
curl -L https://nixos.org/nix/install | bash -s -- --no-daemon
mkdir -p /root/.config/nix
echo "experimental-features = nix-command flakes" > /root/.config/nix/nix.conf
echo ". /root/.nix-profile/etc/profile.d/nix.sh" >> /root/.bashrc
```

### Step 3: Set Up Agent RPC Scaffold (Optional)

If the golden image will run coding agents, set up the FIFO infrastructure and systemd auto-start:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# Create the startup script
mkdir -p /root/bin
cat > /root/bin/start-pi-rpc.sh << 'EOF'
#!/bin/bash
set -e
mkdir -p /tmp/pi-rpc
[ -p /tmp/pi-rpc/in ] || mkfifo /tmp/pi-rpc/in
touch /tmp/pi-rpc/out /tmp/pi-rpc/err

if ! tmux has-session -t pi-keeper 2>/dev/null; then
    tmux new-session -d -s pi-keeper "sleep infinity > /tmp/pi-rpc/in"
fi
if ! tmux has-session -t pi-rpc 2>/dev/null; then
    tmux new-session -d -s pi-rpc "pi --mode rpc < /tmp/pi-rpc/in >> /tmp/pi-rpc/out 2>> /tmp/pi-rpc/err"
fi
echo "Pi RPC scaffold ready"
EOF
chmod +x /root/bin/start-pi-rpc.sh
```

Create systemd units for auto-start on VM restore:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
cat > /etc/systemd/system/pi-rpc.service << 'EOF'
[Unit]
Description=Pi Coding Agent RPC Infrastructure
After=network.target

[Service]
Type=oneshot
User=root
ExecStart=/root/bin/start-pi-rpc.sh
EnvironmentFile=-/etc/environment
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/pi-rpc.path << 'EOF'
[Unit]
Description=Start pi-rpc when environment is written

[Path]
PathModified=/etc/environment
Unit=pi-rpc.service

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable pi-rpc.path
systemctl enable pi-rpc.service
systemctl start pi-rpc.path
```

<Info>
  **Why `.path` units?** Vers snapshots preserve memory state, not boot state. When a VM is restored from a commit, systemd doesn't re-run start jobs. The `.path` unit watches for `/etc/environment` being written (which Vers does on every VM create), triggering service startup with fresh environment variables.

  The `.path` unit must be **active** (not just enabled) at commit time, or it won't fire on restore.
</Info>

### Step 4: Hygiene Pass

Before committing, clean secrets and regenerate identity:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# Stop any running agent sessions
tmux kill-session -t pi-rpc 2>/dev/null || true
tmux kill-session -t pi-keeper 2>/dev/null || true

# Clear history and logs
rm -f /root/.bash_history /root/.zsh_history
> /etc/environment
journalctl --rotate 2>/dev/null || true
journalctl --vacuum-time=1s 2>/dev/null || true
rm -rf /tmp/* /var/tmp/*

# CRITICAL: Regenerate SSH host keys
# Without this, every VM branched from this snapshot shares the same SSH identity
rm -f /etc/ssh/ssh_host_*
ssh-keygen -A

# Clean caches and logs
npm cache clean --force 2>/dev/null || true
rm -rf /root/.npm/_logs 2>/dev/null || true

# Trim filesystem
fstrim -av 2>/dev/null || true

# Secret scan (excluding SSH host keys)
HITS=$(grep -rEao \
   --exclude-dir=.git --exclude-dir=node_modules \
   "gh[pousr]_[A-Za-z0-9]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AKIA[A-Z0-9]{16}" \
   /etc /root /home 2>/dev/null | grep -v "/etc/ssh/" || true)

if [[ -n "$HITS" ]]; then
  echo "WARNING: Potential secrets detected:"
  echo "$HITS" | head -5
  echo "Remove these before committing!"
else
  echo "Hygiene scan clean"
fi
```

<Warning>
  **Vers snapshots capture everything:** RAM, process state, disk, logs. Any secret present at commit time is baked into the image and available to anyone who boots from it. Always stop services and clear `/etc/environment` before committing.
</Warning>

Exit the VM:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
exit
```

### Step 5: Commit and Tag

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers commit golden-builder
# Output: Commit ID: c1a2b3c4...
```

Save the commit ID. You can now boot new VMs from this snapshot:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run-commit c1a2b3c4... -N my-agent-1
```

### Step 6: Clean Up the Builder

The builder VM used generous resources. Delete it once you have the commit:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers delete -y golden-builder
```

## Using Golden Images

### Boot from a Commit ID

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run-commit <commit-id> -N worker-1
```

### Share with Team Members

Share the commit ID directly. Anyone with Vers access can boot from it:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# Team member runs:
vers run-commit c1a2b3c4... -N my-env
```

## Updating Golden Images

When dependencies change, build a new golden image:

1. Boot a fresh VM from the old golden image
2. Update what changed (`npm update`, `apt upgrade`, etc.)
3. Re-run the hygiene pass
4. Commit as a new snapshot

Keep a record of commit IDs and what they contain. A simple convention:

```
# golden-images.md
| Date       | Commit ID        | Contents                          |
|------------|------------------|-----------------------------------|
| 2026-04-19 | c1a2b3c4...      | Node 22, pi, Claude Code, RPC     |
| 2026-04-15 | d5e6f7g8...      | Node 22, pi only                  |
```

## Key Principles

**Stop services before commit.** Running services hold environment variables in memory. When someone boots from the snapshot, the service resumes with the old env vars, ignoring the new VM's `/etc/environment`. Always stop, commit, and let systemd `.path` units restart with fresh env.

**Regenerate SSH host keys.** Without this, every VM from the snapshot has the same SSH identity. Security risk.

**Clear `/etc/environment`.** Vers writes environment variables here on VM create. If the file already has content at commit time, the `.path` unit may not detect a change (same content = no mtime change = no trigger).

**Size the builder generously, the image inherits only what's on disk.** A 30 GB builder VM with 8 GB RAM can produce a golden image that runs fine on 2 GB RAM and 10 GB disk, as long as the installed software fits.

## See Also

* [Agent Bootstrap](/agent-bootstrap) for initial Vers setup
* [Remote Subagents](/agents/remote-subagents) for driving agents on VMs
* [Configuration](/config) for `vers.toml` reference
