Files
devcontainer-stack/README.md
2026-09-12 09:38:07 +02:00

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

  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.