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

# Custom VM Images

> Add Go, Python, Docker, or any other toolchain to your agent VMs with a recipe so they boot ready to build, test, and ship your code.

Every [agent](/concepts/agents) runs on a VM. The platform's default [Image](/concepts/images) gets you Debian, Node, Git, Claude Code, and `gh`, which is enough for most TypeScript or Node projects. If your repos need anything else (Go, Python, Docker, CUDA, a private SDK, ImageMagick, an internal CLI), you'll want a **custom Image** so agents boot with your toolchain already in place.

You can also install things at boot time with a startup script. Here's how to choose:

| You need                                                        | Use              | Why                                                                           |
| --------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- |
| A few small packages, you're still iterating on what to install | A startup script | Easy to change. Re-runs every boot, so it adds a minute or two to spawn time. |
| A heavy toolchain (Go, Python, Docker, CUDA) that's stable      | A baked Image    | Built once, cached. Agents boot instantly with everything already installed.  |

Most teams start with a startup script while figuring out their stack, then move stable installs into a [Recipe](/concepts/recipe) and bake when things settle. See [recipe](/catalog/recipe) for every field on the Recipe resource.

## 1. Write a Recipe

A **Recipe** is a YAML file that names a base Image and a shell script. The shell script is what gets baked into your new Image. Apply it with [`murmur set`](/cli/set):

```bash theme={null}
cat <<'EOF' | murmur set recipe go-python
name: go-python
base_image_gce_ref: murmur-base-v1
provisioning_script: |
  #!/bin/bash
  set -euo pipefail
  curl -fsSL https://go.dev/dl/go1.22.4.linux-amd64.tar.gz | tar -C /usr/local -xz
  echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile.d/go.sh
  apt-get update && apt-get install -y python3 python3-pip python3-venv
  pip3 install pytest requests flask
  apt-get clean && rm -rf /var/lib/apt/lists/*
provisioning_timeout: "30m"
EOF
```

A few things worth knowing about the script:

* **Make it idempotent.** Bake retries from scratch on a fresh VM, so a flaky `apt-get` or network blip can fail one attempt and succeed the next. `set -euo pipefail` and explicit cleanup (`apt-get clean`) keep things tidy and surface real failures.
* **Clean up after yourself.** Anything you leave on disk ends up in the final Image, which makes it bigger and slower to load. Delete tarballs, package caches, and build artifacts before the script exits.

### If your install needs a private registry

If your script needs a token for a private registry (Artifactory, npm Enterprise, a private GitHub repo), add the secret name to the Recipe's `secret_allowlist`. The secret is injected as an environment variable **during the bake only**. It's never written into the final Image, so anyone in your tenant can safely use the resulting Image without seeing your credentials.

## 2. Bake the Image

Kick off the build with [`murmur bake`](/cli/bake):

```bash theme={null}
murmur bake go-python default us-central1
```

A bake takes 5 to 15 minutes. Check in any time with [`murmur bakes ls`](/cli/bakes-ls):

```bash theme={null}
murmur bakes ls
```

Baked Images are content-addressed: their identity is a SHA-256 hash of the base Image, the script, and the target architecture. That means **a no-op rebake is free** because the platform recognizes the inputs haven't changed and reuses the existing Image. Change one byte of your script and you get a fresh Image without having to manage versions yourself.

## 3. Use your new Image

Edit your [Workspace](/concepts/workspaces) to reference the baked Image by name:

```yaml theme={null}
image_ref: go-python
```

The next agent you spawn against this Workspace boots from your new Image. Rolling back is just as easy: edit the field to point at a previous Image and the next agent uses that one instead. Workspaces and Images are intentionally decoupled this way so you can experiment without rebuilding.

## Things to watch for when writing your script

### The murmur user contract

Provisioning runs as **root**. Agents run as the unprivileged **`murmur`** user (UID 1000, home directory `/home/murmur`). This split is on purpose: it means a compromised or runaway agent can't install packages, modify system files, or escalate privileges. The tradeoff is that anything your Recipe installs has to be readable and executable by `murmur` at runtime, otherwise the agent will fail silently when it tries to use the tool.

The patterns below cover the common cases.

### Install to system paths

Things in `/usr/local/bin` are on `murmur`'s `$PATH` by default and world-executable, so this works without any extra steps:

```bash theme={null}
curl -fsSLo /usr/local/bin/mytool https://example.com/mytool
chmod +x /usr/local/bin/mytool
```

### Tools that install into a home directory

Some installers (`sdkman`, `pyenv`, `rustup`, etc.) drop files into `~/`. Run them as `murmur`, otherwise they land in `/root` where the agent can't see them:

```bash theme={null}
sudo -u murmur bash -c 'curl -fsSL https://get.sdkman.io | bash'
```

### Config files dropped as root

If your script copies a config file into `murmur`'s home, fix the ownership or `murmur` will hit a permission-denied at runtime:

```bash theme={null}
chown murmur:murmur /home/murmur/.myconfig
```

### Non-standard `$PATH` locations

If you install something to `/opt/...` or anywhere not already on `$PATH`, extend it through `/etc/profile.d/` (not `~/.bashrc`) so every shell `murmur` opens picks it up:

```bash theme={null}
echo 'export PATH=$PATH:/opt/mytool/bin' >> /etc/profile.d/mytool.sh
```

### Don't redo what the base already has

In the dashboard, expand **"What's on \<image>"** under the base Image picker. It shows you the exact provisioning script that built the base, so you can skip redundant `apt install`s and save bake time.
