4.0 KiB
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
- Rename the directory. Nothing reads its name: the scripts locate themselves,
and
devcontainer.jsonuses${localWorkspaceFolderBasename}. stack.conf: one line per repo,directory|setup command|run command. Setup runs once at container create, run starts that repo's dev server..devcontainer/devcontainer.json: onemountsentry per repo, matching those directories. ThencontainerEnvfor whatever the servers read out of the environment, and the VS Code extension list..devcontainer/Dockerfile: the toolchains. It starts from the plainbase:bookwormimage, with commented patterns for adding a language, apt packages and pinned linters.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
- 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.
- Once inside the container,
File > Open Workspace from File...onstack.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. ./dev.shstarts every server declared instack.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.