# devcontainer-stack A template for one dev container holding several repos at once, so a single Claude Code session can work across a whole stack (a backend and its frontend, a service and its client library) instead of one repo at a time. Copy this directory next to the repos it should hold, rename it, and edit the four files marked below. ## Layout The repos live next to this directory on the host and are bind mounted into the container as subdirectories of the workspace root: /workspaces/ <- workspace root ├── my-backend/ (bind mount) ├── my-frontend/ (bind mount) └── devcontainer-stack/ (this repo, bind mount) ├── .devcontainer/ ├── stack.conf ├── dev.sh └── stack.code-workspace All of them are siblings under the workspace root, so one Claude Code session started there reaches every repo with no `--add-dir` and no `additionalDirectories` permission entry. They are deliberately not nested inside this repo's own bind mount: mount points created inside a bind mount are made by the Docker daemon as root and write through to the host, which leaves root-owned empty directories behind. ## Adapting it 1. Rename the directory. Nothing reads its name: the scripts locate themselves, and `devcontainer.json` uses `${localWorkspaceFolderBasename}`. 2. `stack.conf`: one line per repo, `directory|setup command|run command`. Setup runs once at container create, run starts that repo's dev server. 3. `.devcontainer/devcontainer.json`: one `mounts` entry per repo, matching those directories. Then `containerEnv` for whatever the servers read out of the environment, and the VS Code extension list. 4. `.devcontainer/Dockerfile`: the toolchains. It starts from the plain `base:bookworm` image, with commented patterns for adding a language, apt packages and pinned linters. 5. `stack.code-workspace`: one folder entry per repo. Steps 2 to 5 repeat the repo directories in four files. Keeping them in sync by hand is the price of `devcontainer.json` not being able to read a config file. ## Use 1. Open this folder in VS Code. When prompted, choose **Reopen in Container**, not "Open Workspace": the latter opens the workspace with host paths and bypasses the container entirely. 2. Once inside the container, `File > Open Workspace from File...` on `stack.code-workspace`. The separate roots make each language server root itself at the manifest it expects. Skipping this step still works; the language servers are just less precise. 3. `./dev.sh` starts every server declared in `stack.conf`. Ctrl-C stops all of them. The container uses host networking, so the host browser reaches the servers on `localhost` at whatever ports they bind, with no `forwardPorts` list to keep in sync. ## Notes **Signals.** `dev.sh` sends `SIGTERM` to the process it started. A wrapper that does not forward signals leaves the real server running and holding its port after Ctrl-C. `go run` is one: build a binary and run that. Calling a dev server's binary directly (`./node_modules/.bin/vite`) rather than through a package manager script is one process less to orphan. **pnpm.** `pnpm` records the absolute install path in `node_modules` and asks to purge it when that path changes, which it does when the same repo was installed on the host or in a differently-mounted container. `post-create.sh` has no TTY, so the prompt aborts the whole create. Pass `--config.confirmModulesPurge=false`. **Dev state.** Point the servers at paths inside their own repo (`.dev/` is the convention here, and `stack.code-workspace` hides it) rather than at the `/data`, `/cache` paths their release images expect. The state then disappears with the repo and survives container rebuilds. **Per-repo containers.** A repo that has its own `.devcontainer/` keeps it, for single-repo work and to stay independently buildable. This container supersedes them when working across the stack; don't run both at once, since they bind the same ports.