devlanes is in beta and not ready for production use.Report an issue

Every worktree gets a lane: its own ports, hostnames and data.

Lane 0 follows the primary branch. Any other worktree becomes a lane the first time devlanes runs in it, with the next index, a block of ports and a domain such as myapp1.localhost. One committed file describes the stack for all of them.

A block of ports and a name for each worktree.

Each lane gets ten consecutive ports from one machine-wide pool: 20000 to 20009 for main, 20010 to 20019 for the checkout branch. Every service has the same offset in every lane and reads its port from PORT.

The lane is part of the hostname, api.myapp1.localhost, so each worktree is its own site for cookies and storage. A lane that runs nothing keeps its ports and its data; lane 2 below is idle.

devlanes status
$ devlanes status
myapp
  LANE  BRANCH         STATE    SERVICES  CPU    MEM        URL
  0     main           running  3/3       9.0%   286.0 MiB  https://myapp.localhost
  1     feat/checkout  running  3/3       12.6%  341.0 MiB  https://myapp1.localhost
  2     fix/login      idle     –         –      –          https://myapp2.localhost
  3     agent/perf     running  3/3       38.4%  513.0 MiB  https://myapp3.localhost

One committed file describes the stack.

devlanes.yaml sits at the root of the repository and is committed, so every worktree has it. lane.domain is myapp.localhost in lane 0 and myapp1.localhost in lane 1, so each web app talks to its own API.

Your service only has to listen on $PORT; presets for Vite, Next, Wrangler and others pass the right flags for you. Changes meant for your machine alone go in devlanes.local.yaml, which stays out of Git.

devlanes.yaml
services:
  api:
    run: pnpm --filter api dev
    hosts: "api.${{ lane.domain }}"

  web:
    preset: vite
    run_script:
      file: web/package.json
      name: dev
    env:
      VITE_API_URL: "${{ services.api.public_url }}"
    hosts: "${{ lane.domain }}"

Shared infra, a database per lane.

Containers under infra come from the Compose file you already have and run once for every lane, so ten worktrees do not mean ten Postgres servers. A setup step creates a database per lane inside it, myapp_main for lane 0 and myapp_lane1 for lane 1, and an expression puts its name in DATABASE_URL. A migration in the checkout branch leaves main's data alone.

One command tears a lane down: its database is dropped and its ports freed for the next branch. A branch that needs a container of its own gets one with per_lane.

devlanes.yaml, continued
global_env:
  DATABASE_URL: postgres://postgres:postgres@127.0.0.1:${{ infra.postgres.port }}/${{ lane.db_name }}

lanes:
  vars:
    db_name: "myapp_${{ lane.name }}"

infra:
  compose: docker-compose.yml
  setup:
    database: |
      docker compose exec -T postgres createdb -U postgres ${{ lane.db_name }}
  teardown:
    database: |
      docker compose exec -T postgres dropdb -U postgres --if-exists --force ${{ lane.db_name }}

Edit the file, then reload only what changed.

An edit to devlanes.yaml never touches running services. devlanes shows which ones differ from the file, and a reload restarts only those.

~/dev/myapp-checkout
$ devlanes reload --no-ui
myapp@1 · feat/checkout
✓ services  1 of 1 ready
  ✓  api  https://api.myapp1.localhost

Idle lanes are frozen, so ten of them cost no CPU.

Put agents on ten branches at once and walk away. A lane with no requests, connections or commands for 15 minutes is suspended with the operating system's own mechanism: its ports, data and memory stay, its processes stop using CPU, and the next request wakes it. Overnight, the lanes you are not looking at are idle for the machine.

devlanes status, the next morning
$ devlanes status
myapp
  LANE  BRANCH         STATE              SERVICES  CPU   MEM        URL
  0     main           running            3/3       9.0%  286.0 MiB  https://myapp.localhost
  1     feat/checkout  running frozen 7h  3/3       0.0%  341.0 MiB  https://myapp1.localhost
  3     agent/perf     running frozen 6h  3/3       0.0%  513.0 MiB  https://myapp3.localhost
  4     agent/search   running frozen 8h  3/3       0.0%  402.0 MiB  https://myapp4.localhost

HTTPS on every lane, nothing to set up.

Every hostname is served over HTTPS with a certificate from a CA devlanes creates for you and asks you to trust once. Browsers resolve *.localhost to your machine on their own, so secure cookies, service workers and OAuth callbacks behave on a lane as they do in production. Services get the CA too, so one lane service calling another over HTTPS needs no workaround. When something is off, one command checks the proxy and the certificates and says what to fix.

devlanes doctor
$ devlanes doctor
proxy
  ✓ http on port 80
  ✓ https on port 443
tls
  ✓ CA at /home/you/.devlanes/ca/ca.pem
  ✓ CA trusted by system

In the docs

Install devlanes

Homebrew

brew install devlanes-run/tap/devlanes

Shell script

curl -fsSL https://devlanes.run/install.sh | sh

PowerShell

irm https://devlanes.run/install.ps1 | iex

npm

npm install -g devlanes

uv

uv tool install devlanes

cargo binstall

cargo binstall --git https://github.com/devlanes-run/devlanes devlanes