Skip to content

A preview environment per git worktree

Create a branch, and the environment is already running at a URL of its own. Made to be driven by an agent as readily as by you.

curl -fsSL https://kobune.1024.works/install.sh | sh

kobune.toml

[project]
name = "myapp"
[runtime]
default = "docker"
[services.web]
image = "node:22"
port = 3000
command = "npm run dev"

One file, committed, and read by every worktree the repository grows.

What happens

  1. 1kobune init

    Writes the file above. Nothing else in the repository changes.

  2. 2kobune new feature/user-auth

    Creates the worktree, and brings the environment up with it.

  3. 3https://web.feature-user-auth.myapp.localhost

    Open it. Nobody picked a port, and the name is the same after a restart.

If you are coming from docker compose

The services are the ones you already have. What changes is that they are described per worktree rather than per checkout, and that a service can say it belongs to the project instead of the branch.

docker-compose.yml

services:
web:
image: node:22
command: npm run dev
ports: ["3000:3000"]
db:
image: postgres:16

kobune.toml

[services.web]
image = "node:22"
command = "npm run dev"
port = 3000
[services.db]
image = "postgres:16"
scope = "project"

kobune init --from-compose writes the right-hand file from the left-hand one. Read the TODOs it leaves before the first kobune up — a docker compose file says things Kobune has no key for, and it marks them rather than guessing.

A session, from nothing to two previews

Nothing here is a recording. It is the output the commands print, drawn as text.

Two branches, two URLs, both still running. Nobody picked a port.

The session as text
── shell-a ──
$ kobune init
╭ init ─────────────────────────────────────╮
│ created  ~/myapp/kobune.toml              │
│ project  myapp                            │
│                                           │
│ › bring the environment up with kobune up │
╰───────────────────────────────────────────╯
$ kobune up
  ✓ preparing the network
  ✓ pulling image node:22
  ✓ starting web
  ✓ waiting for web
╭ myapp / (main) ───────────────────────────╮
│ main  ~/myapp                             │
│                                           │
│ ● web  ready  https://web.myapp.localhost │
╰───────────────────────────────────────────╯
$ kobune new feature/user-auth
  ✓ creating worktree feature/user-auth
  ✓ starting web
  ✓ waiting for web
╭ myapp / feature-user-auth ──────────────────────────────────╮
│ feature/user-auth  ~/myapp.wt/feature-user-auth             │
│                                                             │
│ ● web  ready  https://web.feature-user-auth.myapp.localhost │
╰─────────────────────────────────────────────────────────────╯

── web-a ──
https://web.feature-user-auth.myapp.localhost

── shell-a ──
$ cd ~/myapp.wt/feature-user-auth
$ kobune logs -f web
  │ ready in 412 ms
  │ GET /sign-in 200

── shell-b ──
$ kobune new fix/checkout-total
  ✓ creating worktree fix/checkout-total

── shell-a ──
  │ GET /assets/app.js 200

── shell-b ──
  ✓ starting web
  ✓ waiting for web
╭ myapp / fix-checkout-total ──────────────────────────────────╮
│ fix/checkout-total  ~/myapp.wt/fix-checkout-total            │
│                                                              │
│ ● web  ready  https://web.fix-checkout-total.myapp.localhost │
╰──────────────────────────────────────────────────────────────╯

── web-b ──
https://web.fix-checkout-total.myapp.localhost

── shell-a ──
  │ POST /sign-in 302

── shell-b ──
$ kobune ls
╭ workspaces ──────────────────────────────────────╮
│ WORKSPACE           SERVICES  BRANCH             │
│ (main)              1/1       main               │
│ feature-user-auth   1/1       feature/user-auth  │
│ fix-checkout-total  1/1       fix/checkout-total │
╰──────────────────────────────────────────────────╯

What you get

one worktree, one environment
An environment appears with its worktree and goes with it. That correspondence is the whole model.
no ports to remember
Every service gets a URL that survives restarts, while the port underneath changes as it likes.
scale to zero
An untouched environment stops itself and wakes on the next request, in a second or two.
stable names
web.feature-auth.myapp.localhost is built from the branch, so it is the same name tomorrow.
shared where it should be
A database can belong to the project rather than the branch, and be started once for all of them.
share over a tunnel
Put a branch behind a Cloudflare Tunnel and send the link to a phone or a reviewer.

An agent drives it the way you do

The same commands, with --json for the parts a program reads. A failure exits with a code that says what kind it was, so an agent gets a reason rather than an empty response.

Working with AI agents

What runs the containers

One of these goes in [runtime] default, in kobune.toml. Kobune arranges the containers; the runtime is what actually starts them.

  • dockerThe default, and the better supported of the two.Kobune calls the Docker API rather than the docker command, so Docker Desktop, OrbStack and colima all work.
  • appleSupported. Needs macOS 26 or later, with container system start already run.Every container gets its own address, so nothing is published to the host and two worktrees cannot collide on a port.
  • firecrackerPlanned.

What it is not

  • Not a production deployment tool. Everything here assumes a development machine and a person who owns it.
  • Not a container runtime. Docker or Apple Container does that work; Kobune arranges it.
  • Not a replacement for docker compose. If one stack on one branch is all you need, docker compose is simpler and you should keep using it.
How it works

Released under the Apache License 2.0.

Nightly build — not a release, and not stable yet. What that means