guides

Remote lab happy path (host + laptop CLI)

Operator how-to: run grain on a sandbox host, dial it from your laptop with GRAIN_API, create a remote-coding lab, sync code, and tunnel published ports.

Run a durable coding sandbox on a Linux/macOS host, drive it from your laptop CLI. One pass, host then laptop.

For systemd, caps, reverse proxy, and team ops, see Remote sandbox host.

What you will have

MachineRole
Sandbox hostQEMU + grain daemon; VMs live here
Laptopgrain CLI with GRAIN_API / GRAIN_TOKEN; no local hypervisor required

1. Host: install and config

Supported host: Linux or macOS with hardware virtualization (prefer Linux + KVM).

bash
curl -fsSL https://raw.githubusercontent.com/cxdy/grain/main/scripts/install.sh | bash
# install QEMU if needed (grain doctor will say)
grain doctor
grain image pull grain-ubuntu

Write a config with a token and a safe API bind. Prefer loopback:

yaml
# ~/.grain/config.yaml  (or /etc/grain/config.yaml for a service user)
api: 127.0.0.1:7474
api_token: "replace-with-long-random-secret"   # openssl rand -hex 32
BindWhenRequirements
127.0.0.1:7474Default choiceToken still recommended; laptop uses ssh -L
0.0.0.0:7474 (or LAN IP)Direct LAN/VPN dialToken required (daemon refuses without it) + host firewall to VPN/bastion only

Never leave the API open to the public internet without token and network restriction (prefer TLS reverse proxy on loopback instead).

2. Host: start the daemon

bash
grain up
# optional MCP on the host loopback only:
# grain up --mcp
# → http://127.0.0.1:7476/mcp  (do not bind MCP to a public interface without protection)
bash
export GRAIN_TOKEN=replace-with-long-random-secret
curl -sS -H "Authorization: Bearer $GRAIN_TOKEN" http://127.0.0.1:7474/healthz

grain up / down / image * / doctor run on the host (or over SSH). They are not remote-CLI commands.

3. Create a durable lab

On the host, or from the laptop after step 4 is set up:

bash
grain new --profile remote-coding --wait agent -n alice-dev

Builtin remote-coding: persistent (-p), 4 CPU / 8 GiB / 32 GiB, image grain-ubuntu. No host mounts (laptop paths are not on the daemon machine).

Equivalent without the profile:

bash
grain new -p -c 4 -m 8192 -d 32 -i grain-ubuntu --wait agent -n alice-dev

4. Laptop: point CLI at the host

Preferred: tunnel the loopback API.

bash
# terminal 1 — keep open
ssh -N -L 7474:127.0.0.1:7474 sandbox.example.com
bash
# terminal 2
export GRAIN_API=http://127.0.0.1:7474
export GRAIN_TOKEN=replace-with-long-random-secret

grain ls
grain sh alice-dev

If the daemon already binds a reachable LAN URL (token + firewall in place):

bash
export GRAIN_API=http://sandbox.example.com:7474
export GRAIN_TOKEN=replace-with-long-random-secret
grain ls

Transport caveat: that path is cleartext HTTP. The Bearer token authenticates you, but anyone on the network path can sniff Authorization and request bodies. Prefer the SSH tunnel above, or put a TLS reverse proxy in front of 127.0.0.1:7474 and set GRAIN_API=https://sandbox.example.com (CLI uses the system TLS stack; no extra flags). The CLI prints a one-time stderr warning for non-loopback http:// URLs; set GRAIN_INSECURE_HTTP=1 only if you accept cleartext on that path.

Priority: --api flag > GRAIN_API > config api_url.

Install the same grain CLI on the laptop (no QEMU required for remote-only use).

5. Move code with sync (not -v from the laptop)

bash
grain sync push ~/proj alice-dev:/work/proj
grain x alice-dev -- bash -lc 'cd /work/proj && make test'
grain sync pull alice-dev:/work/proj ~/proj
DoDon’t
grain sync push / pull for laptop ↔ guest treesAssume -v /Users/you/... works from the laptop
grain cp file alice-dev:/path for single filesExpect mounts of laptop paths on the remote host

-v / mounts are paths on the sandbox host, not the laptop. A host-side share looks like -v /var/lib/grain/workspaces/alice:/work on the machine running the daemon. For laptop edit loops, use grain sync.

Sync and grain fs need a healthy guest agent (no scp fallback). Prefer --wait agent and the golden image grain-ubuntu.

6. Browser ports: host loopback + second tunnel

Published ports bind 127.0.0.1 on the sandbox host, not your laptop.

bash
# create or add a forward (example: guest 3000 → host 3000)
grain new --profile remote-coding --wait agent -n web -P 3000:3000
# or on an existing VM: grain fwd add web 3000:3000

# laptop: tunnel that host loopback port
ssh -N -L 3000:127.0.0.1:3000 sandbox.example.com
# open http://127.0.0.1:3000

# or print ready-to-run lines for all published + live host ports
grain fwd tunnel web --host sandbox.example.com
# export GRAIN_SSH_HOST=sandbox.example.com  # default for --host

You can combine API and app tunnels:

bash
ssh -N \
  -L 7474:127.0.0.1:7474 \
  -L 3000:127.0.0.1:3000 \
  sandbox.example.com

7. Agent deploy (remote CLI)

CommandRemote CLI (GRAIN_API)
ls, new, rm, stop, start, sh, x, cp, sync, fs, fwd, statsYes (agent ops via daemon proxy)
grain agent deployYes — daemon runs SSH deploy on the host (POST /vms/{name}/agent/deploy)
up, down, image *, doctor, logs, proxy *No — run on host

The agent binary must exist on the daemon host (just agent-linux or ~/.grain/agent/grain-agent-linux-$arch), not on the laptop. Prefer golden grain-ubuntu with --wait agent so deploy is rarely needed.

8. Security (non-negotiable)

  • Never open API or MCP without a shared secret and network controls.
  • Prefer api: 127.0.0.1:7474 + SSH tunnel (or TLS reverse proxy → https://…) over public binds.
  • Non-loopback api requires api_token or the daemon will not start.
  • Remote CLI to a non-loopback URL requires GRAIN_TOKEN / api_token.
  • Cleartext Bearer is sniffable on non-loopback http:// — token ≠ encryption. Tunnel or terminate TLS.
  • CLI warns once on non-loopback cleartext http://; silence only with GRAIN_INSECURE_HTTP=1 when you accept the risk.
  • Keep MCP on 127.0.0.1 unless you know how you will authenticate and firewall it.
  • One Bearer token ≈ one trust domain; this is a team lab pattern, not multi-tenant SaaS and not multi-user RBAC. Hostile co-tenants need separate OS users/data_dir or separate hosts (single-tenant model).
  • Guest agent ops go through the authenticated daemon proxy; agent hostfwd stays loopback-only — do not tunnel raw :7475 agent ports as a substitute for API auth.
  • Default networking is isolated SLIRP. network: overlay is a shared L2: peers can control each other’s unauthenticated agents — only for cooperative labs (overlay security note).

Day-to-day cheat sheet

bash
# host (once)
grain up
grain image pull grain-ubuntu

# laptop (each session)
ssh -N -L 7474:127.0.0.1:7474 sandbox.example.com   # other terminal
export GRAIN_API=http://127.0.0.1:7474
export GRAIN_TOKEN=
grain ls
grain new --profile remote-coding --wait agent -n alice-dev   # once
grain sync push ~/proj alice-dev:/work/proj
grain sh alice-dev
grain stop alice-dev    # free RAM; disk kept (persistent profile)
grain start alice-dev

See also