Skip to content
Skills Library
DevOps & cloud · Automation

worktree-devproxy

/worktree-devproxy

Set up and operate a git-worktree parallel web-dev environment behind a shared Traefik proxy — every worktree runs its own docker compose stack, reachable at http://<worktree>.<repo>.localhost:8000 by Host-header routing, with zero host-port publishing, zero DNS, zero TLS, zero port allocation. Covers: one-time machine setup (Traefik on the `devproxy` network + the `wt` manager script + direnv), onboarding a repo (.envrc, compose Traefik labels, framework adaptations for Vite/Next/etc.), daily usage (wt new/rm/ls), host-run stacks joining the same routing without containers (Traefik file provi

Repository
dev-skills ↗
Licence
MIT
Last repository change
18.10.2026

Assessment and security

87/ 100Class: good
Format20 / 25
Frontmatter with name and description, valid name, description up to 1,024 characters, name matching the folder.
Discoverability17 / 25
Description explains when to use the skill, has a reasonable length and uses third person.
Content35 / 35
At least 150 words and at most 500 lines, headings, step lists, examples or code.
Hygiene15 / 15
No placeholders such as TODO, no Windows paths, with a licence.
Riskno signals detected in the scan
Files in this package
4
Scripts
0
HTML comments
0
URLs in SKILL.md
5
Deprecation

No signals detected. Repository changed on 18.10.2026, without references to retired models or APIs.

Licence

MIT, open: use and modify while retaining author attribution.

What it does and when to use it

Set up and operate a git-worktree parallel web-dev environment behind a shared Traefik proxy — every worktree runs its own docker compose stack, reachable at http://<worktree>.<repo>.localhost:8000 by Host-header routing, with zero host-port publishing, zero DNS, zero TLS, zero port allocation. Covers: one-time machine setup (Traefik on the `devproxy` network + the `wt` manager script + direnv), onboarding a repo (.envrc, compose Traefik labels, framework adaptations for Vite/Next/etc.), daily usage (wt new/rm/ls), host-run stacks joining the same routing without containers (Traefik file provi

Original author description

Set up and operate a git-worktree parallel web-dev environment behind a shared Traefik proxy — every worktree runs its own docker compose stack, reachable at http://<worktree>.<repo>.localhost:8000 by Host-header routing, with zero host-port publishing, zero DNS, zero TLS, zero port allocation. Covers: one-time machine setup (Traefik on the `devproxy` network + the `wt` manager script + direnv), onboarding a repo (.envrc, compose Traefik labels, framework adaptations for Vite/Next/etc.), daily usage (wt new/rm/ls), host-run stacks joining the same routing without containers (Traefik file provi

How it works

  1. One-time machine setup
  2. Onboarding a repo
  3. Daily usage
  4. Host-run stacks (no containers)
  5. Troubleshooting
Original SKILL.md headings · 140 lines, 1,186 words
SKILL.md preview
---
name: worktree-devproxy
description: >
  Set up and operate a git-worktree parallel web-dev environment behind a
  shared Traefik proxy — every worktree runs its own docker compose stack,
  reachable at http://<worktree>.<repo>.localhost:8000 by Host-header
  routing, with zero host-port publishing, zero DNS, zero TLS, zero port
  allocation. Covers: one-time machine setup (Traefik on the `devproxy`
  network + the `wt` manager script + direnv), onboarding a repo (.envrc,
  compose Traefik labels, framework adaptations for Vite/Next/etc.), daily
  usage (wt new/rm/ls), host-run stacks joining the same routing without
  containers (Traefik file provider + devproxy-route + auto port
  allocation), and the known failure modes (Docker API version,
  wrong-network 504s, corporate proxy hijacking localhost, root-owned
  bind-mount files, stale host-run routes). Triggers on: "worktree dev
  environment", "devproxy", "wt new", "parallel dev stacks", "Traefik
  localhost routing", "onboard this repo to the worktree proxy", "set up
  worktree dev on this machine", "<name>.<repo>.localhost", "register
  just dev / pnpm dev with the proxy", or debugging a 404/502/504 from
  the dev proxy.
metadata:
  author: onsager-ai
  version: 0.2.0
---

# worktree-devproxy

One shared Traefik + one compose stack per git worktree. Each instance is reachable at `http://<worktree>.<repo>.localhost:8000`; a laptop reaches it through a single SSH tunnel. Nothing else touches the host network.

```
laptop browser → SSH tunnel (:8000) → Traefik (127.0.0.1 only)
              → Host-header match → per-worktree compose stack (devproxy network)
```

Invariants (don't trade these away when adapting):

1. **No host ports** except Traefik's own `127.0.0.1:8000` (web) and `127.0.0.1:8080` (dashboard/API). Everything else flows over the shared external docker network `devproxy`.
2. **Routing is Host-header only** — `*.localhost` resolves to loopback by OS convention, so there is no DNS, no TLS, and no port allocation table.
3. **Worktree isolation is `COMPOSE_PROJECT_NAME`** (set by `.envrc` via direnv): independent networks, volumes, and container names per worktree.

## One-time machine setup

Prerequisites: `docker` (with compose plugin), `git`, `direnv` (hook it into the shell rc).

1. Copy [`references/dev-proxy.docker-compose.yml`](references/dev-proxy.docker-compose.yml) to `~/dev-proxy/docker-compose.yml`, `mkdir -p ~/dev-proxy/dynamic`, and `docker compose up -d` (run with `COMPOSE_PROJECT_NAME=dev-proxy` or from a direnv-free shell — a stray project name from another repo's `.envrc` creates a *second* Traefik that loses the :8000 bind race). Two flags in it are load-bearing, learned the hard way:
   - `image: traefik:v3` (not an old pin) — Traefik ≤ v3.3 hardcodes
     Docker API 1.24, which Docker Engine 29+ rejects outright.
   - `--providers.docker.network=devproxy` — app containers sit on
     `[default, devproxy]`; without the pin Traefik dials the default
     network's IP it can't reach and every request 504s.
2. Install [`scripts/wt`](scripts/wt) and [`scripts/devproxy-route`](scripts/devproxy-route) to `~/bin/`, `chmod +x`, make sure `~/bin` is on PATH.
3. Verify: `curl -s --noproxy '*' localhost:8080/api/version` returns the Traefik version; `curl localhost:8000` returns 404 (no routes yet — correct).

## Onboarding a repo

Four pieces per repo, all **committed** — worktrees are created from HEAD, so uncommitted setup files silently don't propagate and `wt new` breaks.

1. **`.envrc`** at the repo root:

   ```bash
   export REPO=<repo-dir-name>
   export WORKTREE=$(basename "$PWD" | tr '/_.' '-')
   export COMPOSE_PROJECT_NAME="${REPO}-${WORKTREE}"
   # Main checkout (dir name == repo) sits at the apex; worktrees are
   # subdomains: <repo>.localhost vs feat-x.<repo>.localhost.
   if [ "$WORKTREE" = "$REPO" ]; then
       export DEV_HOST="${REPO}.localhost"
   else
       export DEV_HOST="${WORKTREE}.${REPO}.localhost"
   fi
   ```

   Then `direnv allow`. (Don't apex-special-case `COMPOSE_PROJECT_NAME` the same way on an already-onboarded repo — renaming the project orphans its existing volumes, including the database.)

2. **`docker-compose.yml`** — the HTTP-serving service gets Traefik labels and **no `ports:` section**; the file joins the external network:

   ```yaml
   services:
     web:
       labels:
         - traefik.enable=true
         - traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${DEV_HOST}`)
         - traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=<container port>
       networks: [default, devproxy]

   networks:
     devproxy:
       external: true
   ```

   Internal services (db, redis) keep only the default network and lose any host-port publishing too. Name volumes with **plain aliases** (no explicit `name:`) so compose prefixes them per project — that *is* the per-worktree data isolation. Give an explicit shared `name:` only to caches where cross-worktree reuse is the point (cargo registry, sccache, pnpm store).

3. **Server must bind `0.0.0.0`** inside the container, and framework Host-checking must admit `.localhost`:
   - **Vite**: `server.allowedHosts: ['.localhost']`; gate `host` on an
     env var (`process.env.VITE_HOST ?? 'localhost'`) so bare host-side
     `pnpm dev` is unchanged; if HMR runs through the proxy, wire
     `server.hmr.clientPort` from an env var and set it to `8000` in
     compose.
   - **Next.js**: dev command gets `-H 0.0.0.0`.
   - Multi-process stacks (API + frontend): either run them in **one
     container** so existing `127.0.0.1` proxy targets keep working, or
     keep an internal reverse proxy (Caddy/nginx) as the labeled service.
   - Rust/Go binaries: pass `--host 0.0.0.0` or the bind env var.

4. **CLAUDE.md** — append a section telling agents the instance is at `http://$DEV_HOST:8000` and verifiable with `curl -s -H "Host: $DEV_HOST" localhost:8000`.

Useful patterns from onboarded repos: single-container pnpm monorepo (arbor), build-then-serve with a one-shot builder service + `service_completed_successfully` dependency (duhem), full multi-service stack with an internal Caddy edge carrying the Traefik labels (onsager).

## Daily usage

```
wt new feat/xyz   # run inside the repo → branch + worktree + stack up
                  # → http://feat-xyz.<repo>.localhost:8000
wt rm  feat/xyz   # compose down -v + remove worktree (chown fallback built in)
wt ls             # all worktree containers across repos
```

Worktrees land at `<repo-parent>/<repo>-wt/<name>`, sibling to the repo.

Laptop side, one tunnel serves every repo and worktree:

```
Host devbox
    HostName <remote>
    LocalForward 8000 127.0.0.1:8000
    LocalForward 8080 127.0.0.1:8080
```

## Host-run stacks (no containers)

When the containerized per-worktree stack is too slow as the primary loop (first build compiles the whole workspace into container-private volumes), a repo's native dev runner (`just dev`, `pnpm dev`, …) can join the same routing via Traefik's **file provider** — already enabled by the machine setup above. Pattern (reference implementation: onsager `just dev`, onsager-ai/onsager#579):

1. **Auto-allocate ports, canonical-first**: try the service's usual port, fall back to a random free one so parallel worktree stacks never collide. Bash picker:

   ```bash
   pick_port() {
     local p=$1
     while (exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null; do
       p=$(( (RANDOM % 20000) + 20000 ))
     done
     echo "$p"
   }
   ```

2. **Register the front door only**: `devproxy-route up <port>` writes `~/dev-proxy/dynamic/$COMPOSE_PROJECT_NAME.yml` mapping `Host($DEV_HOST)` → `http://host.docker.internal:<port>`. Point it at the one process that fronts the rest (e.g. Vite with a dev proxy for `/api`); backends only need to be reachable *by that process*, not by Traefik. `devproxy-route down` in the runner's EXIT trap.

3. **Same framework rules as containers**: the fronting process must bind `0.0.0.0` (host-gateway traffic arrives on a non-loopback interface), allow `.localhost` hosts, and aim its HMR websocket at the proxy port (`clientPort: 8000`) when reached through Traefik.

Containerized (`wt new`) and host-run stacks coexist behind one Traefik — routing is per-hostname, so each worktree independently picks whichever mode fits. Don't run both modes for the *same* worktree at once: two routers with an identical Host rule is a coin-flip.

## Troubleshooting

| Symptom | Cause / fix |
| --- | --- |
| Traefik logs `client version 1.24 is too old` | Old Traefik image vs Docker 29+. Use `traefik:v3` (v3.5+). |
| 502 on a host-run stack's hostname | Stale route file from a hard-killed runner (EXIT trap never ran), or the fronting process bound to `127.0.0.1` so host-gateway can't reach it. `rm ~/dev-proxy/dynamic/<project>.yml` or restart the runner; bind `0.0.0.0`. |
| Every route 504s but containers are healthy | Traefik dialing the wrong network. Ensure `--providers.docker.network=devproxy`. |
| 404 from Traefik | No router matched: label interpolation empty (direnv not allowed / compose run without the env), or container not on `devproxy`. Check `curl -s localhost:8080/api/rawdata`. |
| `curl localhost:8000` exits 000 / hangs | Corporate proxy env (`ALL_PROXY` etc.) hijacking loopback. `export NO_PROXY=localhost,127.0.0.1,.localhost` in the shell rc, or `curl --noproxy '*'`. |
| Vite answers 403 / "host not allowed" | Missing `server.allowedHosts: ['.localhost']`. |
| `wt rm` fails `Permission denied` | Root-owned files written by containers into the bind mount. The bundled `wt` already chowns via a throwaway alpine container and falls back to `rm -rf` + `git worktree prune`. |
| `wt new` works but the app 404s/504s briefly | First boot is still installing/compiling inside the container — `docker logs <project>-<service>-1` to watch progress. |
| Stack vanished after a docker daemon restart | App services carry no `restart:` policy by design (only Traefik does). Re-run `docker compose up -d` in the worktree. |
Files in this package (4)
  • SKILL.md 10 KB
  • references/dev-proxy.docker-compose.yml 1 KB
  • scripts/devproxy-route 1 KB
  • scripts/wt 1 KB

Versions

The database has no other skills named worktree-devproxy.

Database snapshot: 13.11.2026 · package version 7b38b548355a (SHA-256 digest of the entire package)

Downloads in the last 30 days: 0 · ranking

Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.

Similar skills

  • 100traefik
    Deploy, configure, and troubleshoot Traefik v3 reverse proxy — covers all providers, routing, TLS/ACME, middlewares, and production patterns with YAML examples. Load when setting up or debugging a Traefik instance. Do not use this skill for unrelated requests; route to the nearest named specialist.
  • 100traefik
    Traefik v3 cloud-native reverse proxy. Covers providers, entrypoints, routers, middlewares, services, Docker labels, TLS/ACME, dashboard, and metrics. USE WHEN: user mentions "traefik", "traefik v3", "traefik docker", "traefik labels", "traefik middleware", "traefik dashboard",…
  • 100traefik
    Route traffic and manage TLS with Traefik reverse proxy. Use when a user asks to set up a reverse proxy, route traffic to Docker containers, auto-provision SSL certificates with Let's Encrypt, set up load balancing, configure HTTPS for multiple services, use a modern alternative…
  • 100traefik
    Use when setting up Traefik as a local reverse proxy — real domains on 127.0.0.1, trusted HTTPS via mkcert, automatic service discovery, and multi-project routing.
  • 100vps-docker-traefik-deploy
    Prepare or operate production Docker Compose deployments on a VPS with Traefik, DNS, registries, persistent storage, backups, and rollback. Use for production infrastructure and releases, rather than local development.
  • 100traefik
    Expert Traefik edge router assistance covering dynamic configuration, Docker/Kubernetes providers, Let's Encrypt TLS, and middlewares. Use when building dynamic reverse proxies and microservice ingress routing.

Feedback and reports

Your message goes to Bartosz Sokoliński. An email address is needed only for a reply. See the privacy policy.