https://github.com/kenfdev/devcontainer-wt
https://github.com/kenfdev/devcontainer-wt
Last synced: 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/kenfdev/devcontainer-wt
- Owner: kenfdev
- Created: 2026-02-19T21:33:00.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-06T02:30:50.000Z (4 months ago)
- Last Synced: 2026-03-06T06:57:37.001Z (4 months ago)
- Language: Shell
- Size: 78.1 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# devcontainer-wt
Seamless devcontainer + git worktree workflows. Run multiple feature branches simultaneously, each in its own isolated devcontainer with its own database, routed via Traefik subdomains.
## What You Get
- **Git works inside containers** -- worktree `.git` file resolution is fixed automatically via volume mount (no file mutation).
- **No port conflicts** -- Traefik routes by subdomain, so every worktree container can listen on the same internal port.
- **Per-worktree database** -- each worktree gets its own database, created automatically on startup.
- **Per-worktree env vars** -- `.env.app.template` is expanded per worktree with `${WORKTREE_NAME}`, `${BRANCH_NAME}`, `${PROJECT_NAME}`, etc.
- **Worktree CLI** -- `./worktree.sh` manages the full worktree lifecycle (create, remove, list, prune) with cleanup hooks.
## Install
Run this from your project's root directory:
```bash
curl -fsSL https://raw.githubusercontent.com/kenfdev/devcontainer-wt/main/install.sh | bash
```
### Minimum mode
For projects that don't need Traefik routing or shared infrastructure (CLI tools, libraries, etc.), use `--minimum`:
```bash
curl -fsSL https://raw.githubusercontent.com/kenfdev/devcontainer-wt/main/install.sh | bash -s -- --minimum
```
Minimum mode gives you:
- Git worktree fix (the primary reason to use devcontainer-wt)
- Per-worktree containers with full isolation
- Per-worktree env vars (via `.env.app.template`)
- Worktree CLI (`./worktree.sh`) for lifecycle management
- Orphan container detection
What it skips:
- No Traefik reverse proxy (access your app via VS Code port forwarding)
- No shared infrastructure services (no Docker Compose profiles)
- No custom Docker network (each worktree uses its own default network)
You can upgrade to full mode later by re-running the installer without `--minimum`.
### Full mode (default)
The installer will:
- Download the template files from GitHub
- Set up `.devcontainer/` with all required configuration
- Prompt to backup if `.devcontainer/` already exists
- Optionally install AI skill files for agent-assisted customization
After installing, see **[CUSTOMIZING.md](.agents/skills/devcontainer-wt/references/CUSTOMIZING.md)** for which files to edit and which to leave alone.
## URL Pattern
All URLs follow this pattern:
```
http://{BRANCH_NAME}.{PROJECT_NAME}.localhost
```
- **`PROJECT_NAME`** = your main repo's directory name (e.g., if you cloned into `myapp/`, the project name is `myapp`).
- **`BRANCH_NAME`** = the current git branch name, sanitized for DNS (slashes become hyphens, e.g., `feature/login` becomes `feature-login`).
For example, if you clone the repo into a directory called `myapp`:
| What | URL |
|---|---|
| Main worktree app (branch `main`) | `http://main.myapp.localhost` |
| Feature worktree (branch `feature-x`) | `http://feature-x.myapp.localhost` |
| Traefik dashboard | `http://traefik.myapp.localhost` |
> **Tip:** After the container starts, check `.devcontainer/.env` to see the resolved values for `PROJECT_NAME` and `BRANCH_NAME`. These determine your URLs.
## Prerequisites
| Requirement | Notes |
|---|---|
| **Docker Desktop** (macOS) or **Docker Engine** (Linux) | Must be running before you start. |
| **VS Code** + **Dev Containers extension** | Install [ms-vscode-remote.remote-containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). |
| **git** | Any recent version with worktree support. |
| **envsubst** | Pre-installed on most Linux. On macOS: `brew install gettext`. |
## Directory Structure
```
myapp/ # <-- you are here (main worktree)
.git/ # git database (directory)
.devcontainer/
devcontainer.json # devcontainer configuration
docker-compose.yml # per-worktree app service
docker-compose.infra.yml # shared infra (Traefik, Postgres) -- profiles-gated
Dockerfile # container image
init.sh # host-side init script
.env # generated (gitignored)
.env.app # generated (gitignored)
.env.app.template # per-worktree env var template (tracked)
worktree.sh # worktree lifecycle CLI
myapp-feature-x/ # worktree (sibling directory)
.git # file pointing to ../myapp/.git/worktrees/feature-x
.devcontainer/ # same files (tracked in git)
src/ # same code, different branch
```
## Step 1: Clone and Open the Main Worktree
The main worktree **must be started first**. It runs the shared infrastructure (Traefik, Postgres).
```bash
# Clone the repo (the directory name becomes your PROJECT_NAME)
git clone myapp
cd myapp
```
Open the folder in VS Code:
```bash
code .
```
VS Code will detect `.devcontainer/devcontainer.json` and show a notification:
> **Folder contains a Dev Container configuration file.** Reopen folder to develop in a container.
Click **"Reopen in Container"** (or run the command palette: `Dev Containers: Reopen in Container`).
### What Happens Behind the Scenes
1. **`init.sh` runs on the host** (via `initializeCommand`):
- Detects this is the main worktree (`.git` is a directory).
- Derives `PROJECT_NAME` from the directory name (e.g., `myapp`).
- Sets `WORKTREE_NAME` to the directory name (e.g., `myapp`).
- Sets `BRANCH_NAME` from the current git branch (e.g., `main`).
- Writes all resolved values to `.devcontainer/.env`.
- Sets `COMPOSE_PROFILES=infra` so Traefik (and any infrastructure services you've added) start.
- Creates Docker network `devnet-{PROJECT_NAME}`.
- Expands `.env.app.template` into `.devcontainer/.env.app`.
2. **Docker Compose brings up containers**:
- `traefik-{PROJECT_NAME}` -- reverse proxy on port 80 (configurable).
- Any infrastructure services you've added (Postgres, Redis, etc.).
- `app-{PROJECT_NAME}-{WORKTREE_NAME}` -- your app container.
3. **Git works immediately** -- the git common directory is mounted at the same absolute host path, so `.git` file references resolve directly inside the container.
### Verify It Works
First, check the generated values:
```bash
cat .devcontainer/.env
# Look for PROJECT_NAME and BRANCH_NAME -- these determine your URLs.
```
Then open your browser. Assuming you cloned into `myapp/`:
| URL | What It Shows |
|---|---|
| http://main.myapp.localhost | Your app (main worktree) |
| http://traefik.myapp.localhost | Traefik dashboard (shows all routes) |
Your app should be reachable if you've set up a dev server. The sample app shows project/worktree info.
> **Note (macOS):** `*.localhost` resolves to `127.0.0.1` out of the box. No `/etc/hosts` changes needed.
>
> **Note (Linux):** If subdomains don't resolve, see [Platform Notes: Linux](#linux-native-docker).
## Step 2: Create a Feature Worktree
With the main worktree running, create a new worktree from the **host terminal** (not inside the container):
```bash
# From the main repo directory
cd myapp
# Create a worktree using the CLI
./worktree.sh add feature-x
```
This creates `myapp-feature-x/` next to `myapp/` with a `.git` **file** (not directory) pointing back to the main repo's git database.
Now open it in VS Code:
```bash
code ../myapp-feature-x
```
> **Tip:** You can also run `./worktree.sh add` without arguments to be prompted for a branch name, or use the standard git command directly: `git worktree add ../myapp-feature-x -b feature-x`.
Click **"Reopen in Container"** again.
### What Happens This Time
1. **`init.sh` runs on the host**:
- Detects this is a worktree (`.git` is a file, not a directory).
- Sets `PROJECT_NAME=myapp` (derived from the main repo, not the worktree directory).
- Sets `WORKTREE_NAME=myapp-feature-x` (from the worktree directory name).
- Sets `BRANCH_NAME=feature-x` (from the current git branch).
- Does **not** set `COMPOSE_PROFILES=infra` -- infrastructure services are NOT started again.
- Joins the existing `devnet-myapp` network.
2. **Only the app container starts**: `app-myapp-myapp-feature-x`.
3. **Git works immediately** -- the git common directory is mounted at the same absolute host path, so the `.git` file's host path references resolve directly. Git commands (`log`, `blame`, `status`, `commit`) work out of the box.
### Verify the Feature Worktree
| URL | What It Shows |
|---|---|
| http://feature-x.myapp.localhost | Your app (feature-x worktree) |
Check the Traefik dashboard at `http://traefik.myapp.localhost` -- you should see routes for both worktrees.
### Verify Git Works Inside the Container
Open a terminal inside the feature worktree's VS Code window and run:
```bash
git status
git log --oneline -5
git branch
```
All commands should work normally, even though this is a worktree inside a container.
## Step 3: Work on Multiple Worktrees Simultaneously
Repeat Step 2 for as many branches as you need:
```bash
# Another feature
./worktree.sh add feature-y
code ../myapp-feature-y
# PR review
git fetch origin
git worktree add ../myapp-pr-42 origin/some-pr-branch
code ../myapp-pr-42
```
Each one gets:
- Its own VS Code window and devcontainer.
- Its own Traefik route: `http://feature-y.myapp.localhost`, `http://some-pr-branch.myapp.localhost`.
- Its own database (if you've configured one).
- Full git support inside the container.
## Step 4: Clean Up a Worktree
Use the CLI to remove a worktree and its container in one step:
```bash
./worktree.sh remove ../myapp-feature-x
```
This will:
1. Run the cleanup hook (`.devcontainer/hooks/on-remove.sh`) for project-specific cleanup (e.g., dropping databases).
2. Stop and remove the worktree's container.
3. Remove the worktree directory (refuses if there are uncommitted changes -- use `git worktree remove --force` manually to override).
4. Prune any other orphaned containers.
To clean up orphaned containers without removing a specific worktree:
```bash
./worktree.sh prune
```
To see all worktrees and their container status:
```bash
./worktree.sh list
```
> **Tip:** Customize `.devcontainer/hooks/on-remove.sh` to add project-specific cleanup (dropping databases, clearing caches). See [CUSTOMIZING.md](.agents/skills/devcontainer-wt/references/CUSTOMIZING.md).
## Customization
### Change the Traefik Port
If port 80 is in use, set `TRAEFIK_PORT` before opening the main worktree:
```bash
export TRAEFIK_PORT=8000
code myapp
```
Then access your app at `http://{BRANCH_NAME}.{PROJECT_NAME}.localhost:8000`.
### Change the Postgres Host Port
```bash
export POSTGRES_HOST_PORT=25432
```
The default is `15432` to avoid conflicts with a host Postgres on `5432`.
### Override the Project Name
By default, the project name is derived from the main repo's directory name. Override it:
```bash
export PROJECT_NAME=my-custom-name
```
This changes all routes (`*.my-custom-name.localhost`), container names, database names, and the Docker network name.
### Add Environment Variables
Edit `.env.app.template` (tracked in git) to add per-worktree variables:
```bash
DATABASE_URL=postgres://dev:dev@postgres-${PROJECT_NAME}:5432/${PROJECT_NAME}_${WORKTREE_NAME}
REDIS_URL=redis://redis-${PROJECT_NAME}:6379/0
APP_NAME=${PROJECT_NAME}-${WORKTREE_NAME}
MY_SECRET=${MY_SECRET} # reads from host env var
```
Each worktree's `init.sh` expands this into `.devcontainer/.env.app` (gitignored).
### Add Infrastructure Services
Edit `.devcontainer/docker-compose.infra.yml` to add services under the `infra` profile. For example, to add Redis:
```yaml
redis:
profiles: [infra]
image: redis:7-alpine
container_name: "redis-${PROJECT_NAME}"
networks:
- devnet
restart: unless-stopped
```
### Change the App Port
If your app listens on a port other than 3000, update the Traefik label in `.devcontainer/docker-compose.yml`:
```yaml
- "traefik.http.services.${PROJECT_NAME}-${WORKTREE_NAME}.loadbalancer.server.port=4000"
```
### Headless Usage (devcontainer CLI / AI Agents)
The template works without VS Code. All lifecycle hooks run the same way:
```bash
# Start a worktree container
devcontainer up --workspace-folder ../myapp-feature-x
# Run commands inside
devcontainer exec --workspace-folder ../myapp-feature-x bash
```
For git authentication, set `GITHUB_TOKEN` on the host:
```bash
export GITHUB_TOKEN=ghp_xxx
devcontainer up --workspace-folder ../myapp-feature-x
```
## How It Works
### The Git Worktree Problem
A git worktree's `.git` is a **file** containing an absolute host path:
```
gitdir: /Users/you/myapp/.git/worktrees/feature-x
```
When mounted into a container at `/workspaces/myapp-feature-x`, this host path doesn't exist. All git commands fail.
### The Volume Mount Fix
Instead of rewriting the `.git` file (which would mutate the bind-mounted file and break host-side git), `docker-compose.yml` mounts the git common directory at the same absolute host path inside the container:
```
Host: /Users/you/myapp/.git/ → Container: /Users/you/myapp/.git/ (same path)
```
When git reads the `.git` file and follows `/Users/you/myapp/.git/worktrees/feature-x`, the path exists inside the container because the volume mount places the git directory at the exact same path.
The `.git` file is **never modified**. No symlink or post-start script needed. The host is unaffected.
### Infrastructure Isolation
- **Main worktree** starts with `COMPOSE_PROFILES=infra`, which activates Traefik and Postgres.
- **Feature worktrees** do not set this profile, so they only start the app container.
- All containers join the same Docker network (`devnet-{PROJECT_NAME}`), so they can reach each other by container name.
- Traefik auto-discovers app containers via Docker labels and routes traffic by subdomain.
## Architecture
```
Browser
|
v
Traefik (port 80)
|
|-- {BRANCH}.{PROJECT}.localhost --> app-{PROJECT}-{WORKTREE}:3000
|-- traefik.{PROJECT}.localhost --> Traefik dashboard
|
Docker Network: devnet-{PROJECT}
|
|-- postgres-{PROJECT}:5432
| |-- DB: {PROJECT}_{WORKTREE_1}
| |-- DB: {PROJECT}_{WORKTREE_2}
|
|-- app-{PROJECT}-{WORKTREE_1} (main worktree container)
|-- app-{PROJECT}-{WORKTREE_2} (feature worktree container)
```
## Platform Notes
### macOS (Docker Desktop)
- `*.localhost` resolves to `127.0.0.1` by default. No configuration needed.
- Chrome works out of the box. Firefox may require `about:config` -> set `network.dns.localDomains` to include your subdomains, or use `/etc/hosts`.
### Linux (Native Docker)
`*.localhost` wildcard resolution may not work. Two options:
**Option A: `/etc/hosts` (manual, per worktree)**
```
127.0.0.1 main.myapp.localhost
127.0.0.1 feature-x.myapp.localhost
127.0.0.1 traefik.myapp.localhost
```
**Option B: `dnsmasq` (automatic wildcard)**
```
# /etc/dnsmasq.d/localhost.conf
address=/localhost/127.0.0.1
```
## Troubleshooting
### "Reopen in Container" does nothing or fails immediately
Check that Docker Desktop is running:
```bash
docker ps
```
### App not reachable at `*.localhost`
1. Check your actual URLs: `cat .devcontainer/.env` to see `PROJECT_NAME` and `BRANCH_NAME`.
2. Check Traefik is running: `docker ps | grep traefik`
3. Check the Traefik dashboard: `http://traefik.{PROJECT_NAME}.localhost`
4. Test with curl: `curl -H "Host: {BRANCH}.{PROJECT}.localhost" http://localhost/`
5. On Linux, check DNS resolution (see [Platform Notes](#linux-native-docker)).
### Git commands fail inside a worktree container
Check if the git common directory is mounted at the correct path:
```bash
# Inside the container
ls -la /Users/ # should see your host path structure
```
Check that the git common directory volume mount is correct in `.devcontainer/.env`.
### Database connection refused
The main worktree must be running (it hosts Postgres). Check:
```bash
docker ps | grep postgres
```
### Port 80 already in use
Set a custom Traefik port before starting:
```bash
export TRAEFIK_PORT=8000
```
Then access apps at `http://{BRANCH}.{PROJECT}.localhost:8000`.
### `envsubst: command not found` (macOS)
```bash
brew install gettext
```
`envsubst` is included in the `gettext` package.
## Limitations
- **Main worktree must start first.** Infrastructure services (Traefik, Postgres) only run from the main worktree.
- **Docker Compose only.** The template requires Docker Compose as the devcontainer backend.
- **Name collisions.** Branch names like `feature/login` and `feature-login` both sanitize to `feature-login`. Use distinct branch names.
- **GitHub Codespaces not supported.** Different constraints (no Traefik, no sibling worktrees).