Run one Linux development environment per Git branch.
  • Go 83.4%
  • Nix 16%
  • Shell 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
OTABI Tomoya aed5b43970
chore(release): bump v0.2.2 (#69)
Cuts a patch so the guest login fix and the forward resolution change
since v0.2.1 reach users pinning a release, along with the
gvisor-tap-vsock 0.9.0 update. There is no feature or breaking change,
so nothing argues for a minor.
2026-10-23 15:31:36 +09:00
.github chore(deps): update dependency ubuntu to v26 (#67) 2026-10-19 13:45:38 +09:00
cmd/sprout chore(release): bump v0.2.2 (#69) 2026-10-23 15:31:36 +09:00
dev chore(deps): lock file maintenance (#68) 2026-10-23 11:24:03 +09:00
docs perf(forward): resolve the target instance only when HEAD changes (#57) 2026-10-07 22:48:27 +09:00
examples docs: document Linux hosts and their differences from macOS (#45) 2026-10-06 00:39:48 +09:00
nix fix(guest): move only sprout's login user into /workspace at login (#58) 2026-10-07 22:48:32 +09:00
skills/sprout docs: document Linux hosts and their differences from macOS (#45) 2026-10-06 00:39:48 +09:00
.gitignore import from private repository 2026-08-13 15:14:37 +09:00
CHANGELOG.md chore(release): bump v0.2.2 (#69) 2026-10-23 15:31:36 +09:00
cliff.toml chore(release): bump v0.2.0 (#48) 2026-10-06 10:02:08 +09:00
flake.lock chore(deps): lock file maintenance (#68) 2026-10-23 11:24:03 +09:00
flake.nix chore(deps): update module github.com/containers/gvisor-tap-vsock to v0.9.0 (#63) 2026-10-11 20:19:25 +00:00
go.mod chore(deps): update module github.com/containers/gvisor-tap-vsock to v0.9.0 (#63) 2026-10-11 20:19:25 +00:00
go.sum chore(deps): update module github.com/containers/gvisor-tap-vsock to v0.9.0 (#63) 2026-10-11 20:19:25 +00:00
LICENSE import from private repository 2026-08-13 15:14:37 +09:00
README.md docs: document Linux hosts and their differences from macOS (#45) 2026-10-06 00:39:48 +09:00

sprout 🌱

Run one Linux development environment per Git branch.

sprout boots NixOS microVMs for local development on Apple Silicon Macs and on Linux hosts with KVM. Each branch gets an independent instance with its own persistent /var, while its worktree is mounted live at /workspace. Open several Git worktrees and run databases, container stacks, or Kubernetes clusters side by side without sharing their runtime state or changing the ports they use inside the guest. It needs Nix with flakes enabled, plus an aarch64-linux builder on a Mac; sprout init writes the flake, so a project starts without Nix code of its own.

Example: a feature branch running next to main

Suppose the project's dev command starts a web app on port 5173 and PostgreSQL on port 5432, and feature/teams changes the account schema and backfills every existing row. You want to click through the branch with today's version still open beside it. Starting the stack twice on one machine does not get you there: the second copy has to move off both ports, and it still talks to the one PostgreSQL, so the branch's backfill also rewrites what main shows.

Instead, give the branch its own worktree and boot the same stack from each:

# Terminal 1 stays in the main worktree
$ sprout up
building and booting "main" in the background (log: …/up.log) …
VM ready. Enter it with: sprout shell
$ sprout exec -- npm run dev
# Terminal 2, starting from the main worktree
$ git worktree add ../project-teams -b feature/teams
$ cd ../project-teams
$ sprout up
building and booting "feature/teams" in the background (log: …/up.log) …
VM ready. Enter it with: sprout shell
$ sprout exec -- npm run dev

Neither dev server had to be told anything about the other. Both bind guest 5173 and 5432, because each VM has its own network. Each writes to its own persistent /var, so the backfill on feature/teams cannot reach main's database. And each mounts the worktree it was booted from at /workspace, so an edit on the host is live in the matching environment with nothing to sync.

sprout list shows the pair (IDs, measurements, and paths vary by host):

$ sprout list
ID            NAME           STATE    UPTIME  CPU  MEM    DISK   WORKSPACE
2493c5c3af70  feature/teams  running  <1m     -    12MiB  67MiB  …/project-teams
887757c37a76  main           running  <1m     -    12MiB  67MiB  …/project

To see both in the browser at once, run one router in a third terminal. It serves every instance on a single host port, addressed by branch name:

$ sprout route serve --port 8080
routing http://<name>.sprout.localhost:8080/ → instances on localhost:8080 (Ctrl-C to stop)
http://5173.main.sprout.localhost:8080/           → main :5173
http://5173.feature-teams.sprout.localhost:8080/  → feature/teams :5173

Reading a URL left to right: the leading 5173 is the guest port to forward to, the label next to sprout.localhost picks the instance, and :8080 is the host port the router bound.

From here on, switching between the old page and the new one is switching browser tabs. When the comparison is over, sprout stop gives an instance's memory back and leaves its database on /var for the next session, and sprout delete discards that branch's runtime state and nothing else. An instance left alone stops itself after two hours without SSH or router traffic (idle.after), and a running router boots it back on the next request.

Why branch-scoped environments

A branch often needs more than another copy of the source tree. Its database, containers, cluster state, and background services must also be independent if two versions are to run at the same time. That independence is what the following cases have in common:

  • Keep a reference running. Boot main next to the branch you are changing and compare the two in a browser, one URL per branch, instead of restarting a stack whenever you need to see the old behavior again.
  • Try several implementations at once. Give each candidate its own branch, whether they differ in schema design, page layout, or library choice, and leave them all up while you decide: compare branches side by side.
  • Run a migration you can undo. A migration reaches only that branch's own /var volume, and a snapshot taken before it restores the volume afterwards.
  • Take a review without putting your own work away. Check the pull request out in another worktree and boot it; your instance keeps running, its services and database untouched. Reproducing a bug against an older commit works the same way.
  • Reuse an environment that was expensive to set up. When the cost sits in a seeded database or a converged cluster rather than the build, sprout fork clones that volume into a new branch instead of repeating the setup.

Container tooling draws this boundary elsewhere. On macOS, Docker Desktop, OrbStack, and colima run every container inside one shared Linux VM; on Linux, Docker and Podman run them on the host's own kernel. Either way a compose project can keep a branch's containers apart, but all branches still compete for the host ports they publish, share one kernel and container runtime, and none has a volume to snapshot or fork as one unit; a workload that assumes a machine of its own (systemd as PID 1, kernel modules, a k3s node) needs per-tool workarounds. Apple's container boots a VM per container, but the unit is still one container, not a branch's stack with its services and state. sprout makes the branch the unit: each gets a whole VM, so it owns everything its stack can touch.

sprout treats the repository and branch as the instance identity, so commands can derive their target without manually assigned VM names.

The guest is a full NixOS system declared in the project flake. It can run systemd, Docker or Podman, k3s, and kernel modules. The flake lock pins the guest definition, while host credentials and build caches are available only when the project declares them.

Quick start

You need Nix with flakes enabled and one of these hosts:

  • Apple Silicon macOS, with an aarch64-linux builder.
  • x86_64 or aarch64 Linux, with /dev/kvm usable by your user and unprivileged user namespaces allowed (Ubuntu restricts them by default); the guest builds locally.

The getting-started tutorial spells out each prerequisite. Check the host before booting a VM:

$ nix shell github:natsukium/sprout
$ sprout doctor --build

In a repository without a flake.nix, generate a minimal VM definition and boot it:

$ sprout init
$ git add flake.nix     # make the new file visible to Nix
$ sprout up
$ sprout exec -- uname -a
$ sprout shell

sprout init does not overwrite an existing flake. The getting-started tutorial shows how to add the same configuration to an existing flake or one that does not use flake-parts.

Once the configuration is committed, run sprout up from another worktree to boot an independent instance as in the example above. The router reaches running HTTP services by instance name; start it with sprout route serve --port 8080. Use sprout forward for raw TCP ports.

Capabilities

  • Independent branch state: one persistent /var volume per instance, with copy-on-write snapshots and forks.
  • Declarative guests: project flake options define resources, credentials, caches, and readiness; ordinary NixOS modules define packages and services.
  • Worktree-aware commands: up, shell, exec, stop, and delete target the current repository and branch by default.
  • Full Linux stacks: run systemd services, nested container runtimes, or a local k3s cluster inside the VM.
  • Named HTTP routing: reach several branches through one host port without editing DNS or /etc/hosts.
  • Scoped host integration: the guest sees the worktree, the read-only host Nix store, and beyond those only the credentials and caches the project declares.

Documentation

Scope and status

sprout supports Apple Silicon macOS hosts (aarch64-linux guests under vfkit) and x86_64-linux and aarch64-linux hosts (guests of the same architecture under QEMU/KVM); the compatibility reference lists what each backend does differently and what is not supported yet. It is in the 0.x.y pre-release line, so the CLI and configuration may change between releases; the changelog says what each release changed and what to change when upgrading (flakes written for 0.1.x expose VMs under a different output), and the compatibility and release policy says what is promised.

sprout is a development environment, not a sandbox for untrusted code. With workspace = true, root in the guest can modify the checkout and its Git metadata. Enable only the shares and credentials the guest needs; the architecture documentation lists every path across the VM boundary.

Development

Run direnv allow or nix develop to enter the contributor shell. nix flake check runs formatting and evaluates the guest modules. go test ./... runs the Go suite and checks that fenced Nix snippets parse and documented sprout commands still match the CLI. dev/check-examples.sh evaluates each runnable example flake without building it; CI runs all three checks.

License

sprout is distributed under the Apache License, Version 2.0.