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
- Author
- onsager-ai ↗
- Repository
- dev-skills ↗
- Licence
- MIT
- Last repository change
- 18.10.2026
Assessment and security
- Files in this package
- 4
- Scripts
- 0
- HTML comments
- 0
- URLs in SKILL.md
- 5
No signals detected. Repository changed on 18.10.2026, without references to retired models or APIs.
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
- One-time machine setup
- Onboarding a repo
- Daily usage
- Host-run stacks (no containers)
- Troubleshooting
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.