get-started

Sandbox recipes (YAML create + bootstrap)

Portable recipe files for create options and bootstrap steps that stamp readiness before grain says ready.

Goal: check in a YAML file that creates a sandbox, runs install steps, and only reports ready when those steps finish with zero failures.

Deep contract: Readiness protocol. Manual cloud-init path: Bootstrap until ready.


1. Minimal recipe

Save as git-lab.recipe.yaml:

yaml
apiVersion: grain/v1
kind: Sandbox
metadata:
  name: git-lab
spec:
  image: grain-ubuntu
  cpus: 2
  memory_mb: 2048
  ready_timeout: 10m
  bootstrap:
    steps:
      - name: packages
        message: installing git
        run: |
          export DEBIAN_FRONTEND=noninteractive
          apt-get update -qq
          apt-get install -y -qq git          
bash
grain up
grain image pull grain-ubuntu
grain recipe validate ./git-lab.recipe.yaml
grain new --recipe ./git-lab.recipe.yaml
grain status git-lab
grain sh git-lab

When bootstrap.steps is set, create defaults to --wait bootstrap. Ready means: VM up, agent healthy, all steps succeeded (state=ready).


2. What a recipe can set

FieldRole
metadata.nameDefault VM name (-n overrides) and default ready_name
spec.image / cpus / memory_mb / disk_gb / persistentCreate resources
spec.presetMerge docker / k3s / act cloud-init
spec.mountsHost shares (. = your current directory)
spec.forwardsPort publish (guest_port, optional host_port)
spec.userdata / userdata_fileExtra cloud-init or shell (merged before bootstrap steps)
spec.bootstrap.stepsOrdered guest scripts; each becomes a readiness phase
spec.wait / ready_timeoutWait mode and create timeout

CLI flags still win over recipe fields (same idea as profiles).

Examples in-repo: examples/recipes/.

From Grain Desktop, select a sandbox → inspector More → Export as recipe… to save create options (image, resources, mounts, forwards) as a recipe file. Bootstrap steps and first-boot userdata are not recovered from a live VM — add those by hand if needed.

Local library (~/.grain/recipes)

Install recipes once, then create by name (CLI, Desktop, MCP):

bash
# Import (never creates a VM)
grain recipe add ./git-lab.recipe.yaml          # → ~/.grain/recipes/git-lab.yaml
grain recipe preview https://example.com/lab.yaml  # validate + summary, no install
grain recipe add https://example.com/lab.yaml   # http(s) YAML
grain recipe search                             # official catalog index only
grain recipe add git-lab                        # pull one official body into the library

grain recipe list
grain recipe show git-lab
grain recipe validate git-lab
grain new --recipe git-lab                      # name resolves under ~/.grain/recipes
grain recipe delete git-lab                     # library file only — not sandboxes

Official catalog recipes

In-repo under recipes/ (index: recipes/catalog.json):

idNotes
git-labMinimal bootstrap (git)
node-devRepo mount + git/curl
python-devpython3/pip/venv bootstrap
go-devGo toolchain bootstrap (git + golang)
docker-labPreset docker
k3s-labPreset k3s + publish 6443
act-labPreset act + mount ./work
remote-codingPersistent 4 vCPU / 8 GiB coding lab

Contributing official recipes: open a PR that adds recipes/<id>.yaml and updates catalog.json (sha256 of the file). No accounts or marketplace backend — GitHub PRs only.

Download counts (no extra infra): when recipe or image bundles ship as GitHub Release assets, use the public Releases API download_count per asset (gh api repos/cxdy/grain/releases / release page). Library code can call internal/recipe.FetchReleaseAssetDownloads for the same numbers. The official catalog index itself is git-sourced (recipes/catalog.json) until a release pin is published.

Fast sandboxes (snapshot spawn + warm pool)

Cold grain new --wait agent is ~seconds (guest boot). For fast labs:

bash
# One-time template
grain new -i grain-ubuntu -n golden -p --wait agent
# ... optional setup ...
grain suspend golden          # qcow2 savevm grain-suspend when possible

# On-demand fast copies (clone disk + -loadvm when snapshotted)
grain new --from golden -n work1
grain new --from golden -n work2

# Warm pool: pre-clone members, claim without re-cloning
# ~/.grain/config.yaml:
#   warm_pool:
#     template: golden
#     size: 2
#     running: false   # optional: true = agent-ready members (RAM)
grain pool fill
grain new --from-pool -n work3   # or: grain pool claim -n work3
grain pool status

API: POST /vms with {"from":"golden","name":"work1"} or {"from_pool":true,"name":"work3"}. Also GET /pool, POST /pool/fill|claim|drain. See lifecycle.

Desktop:

  • Recipes tab → Import file / URL / Browse official → Edit YAML → Deploy… (preflight + name + wait).
  • New sandbox modes: cold / from template / from warm pool (prefers pool when ready > 0).
  • More → Promote to golden + fill pool on a prepared sandbox.
  • More → Export as recipe… / Save as library recipe.

Trust: URL import is preview then add (Desktop fetches and validates, shows name/image/resources/mounts/bootstrap, then you confirm install). Official Add and CLI recipe add <url> install into the library only. Deploy/create is always a separate step. Prefer HTTPS; HTTP is allowed with a warning. Catalog entries may pin sha256 (fail closed on mismatch). Offline: library always works; recipe search uses a cached index when present.

Env overrides: GRAIN_HOME (library under $GRAIN_HOME/recipes), GRAIN_RECIPE_CATALOG_URL (official index URL).


3. Inspect without creating

bash
grain recipe validate ./git-lab.recipe.yaml
grain recipe show ./git-lab.recipe.yaml
grain recipe show git-lab                        # library name
grain recipe show ./git-lab.recipe.yaml --userdata   # compiled cloud-init

4. vs profiles and presets

MechanismPortable file?Bootstrap readiness?
Profile (~/.grain/config.yaml)No (host config)Only if you attach userdata yourself
Preset (docker / k3s / act)Built into grainPreset install scripts (not recipe steps)
Recipe (--recipe)Yes (repo-friendly)Yes — steps stamp readiness automatically

Next