Shared dev container Features so every
repo's containers come with the same baseline — without copy-pasting post-create.sh
across repos.
Installs the AI coding agents we standardize on:
- OpenAI Codex CLI (
@openai/codex, version pinned via thecodexVersionoption) - Claude Code (pulled in automatically via
dependsOnon the officialghcr.io/anthropics/devcontainer-features/claude-codefeature) - GitHub CLI (
gh, viadependsOnon the officialghcr.io/devcontainers/features/github-clifeature)
The feature mounts named volumes (devcontainer-claude-config at /home/node/.claude,
devcontainer-gh-config at /home/node/.config/gh) and sets CLAUDE_CONFIG_DIR, so the
Claude and gh auth login sessions survive container rebuilds — you log in once
instead of after every rebuild. A postCreateCommand chowns the volumes so the node
user can write to them.
The volumes are shared across all repos that use this feature (these logins are account-level, not repo-level), so logging in from one container carries over to the others. The mount paths assume the
noderemote user (the base image we standardize on).
In any repo's .devcontainer/devcontainer.json:
This single line replaces the official claude-code and github-cli feature lines, the
inline npm install -g @openai/codex, and the .claude volume / CLAUDE_CONFIG_DIR /
chown wiring that otherwise lives in each repo's devcontainer.json + post-create.sh.
- Runs
corepack enable(adds thepnpmshim) at build time. - At
postCreate, runscorepack installto pin the pnpm version from the workspace'spackage.jsonpackageManagerfield. - Removes the
npm/npxbinaries (after build-time installs have run) to enforce pnpm-only usage. This is the default; setremoveNpm: falseto keep npm available.
"features": {
"ghcr.io/rocicorp/devcontainer-features/pnpm:1": {}
}This replaces the corepack/pnpm/npm-removal block that otherwise lives in each repo's
post-create.sh. Combined with agents, a consumer repo's devcontainer.json needs no
lifecycle scripts at all.
Gives the container a working Docker daemon so tooling that shells out to Docker — most
notably testcontainers (used by the zero-cache Postgres
integration tests) — runs inside the dev container.
- Pulls in the official
ghcr.io/devcontainers/features/docker-in-dockerfeature viadependsOn, which installs the Docker engine, runs a daemon inside the container, and adds the remote user to thedockergroup (nosudoneeded). - Pins
"moby": falseso the upstream feature installs Docker CE from Docker's own apt repo instead of Microsoft'smoby-*packages, which don't exist on Debian trixie (the base of currentjavascript-nodeimages) and fail the build. - Uses Docker-in-Docker rather than docker-outside-of-docker on purpose: testcontainers relies on bind mounts and container-to-container networking, both of which break under the host-socket approach (path translation) and aren't available in every environment (Codespaces, CI). A self-contained daemon "just works" everywhere.
"features": {
"ghcr.io/rocicorp/devcontainer-features/docker:1": {}
}This replaces a per-repo docker-in-docker feature line and centralizes the pinned version
alongside the other rocicorp features.
- Bump
codexVersiondefault (and/or thedependsOnclaude-code pin) insrc/agents/devcontainer-feature.json, raise the featureversion, merge tomain. The release workflow publishes a new tag toghcr.io. - Consumer repos pick it up on next rebuild. To avoid hand-editing pins, enable
Dependabot (
devcontainersecosystem) in each consumer repo — it opens PRs that bump thedevcontainer-lock.jsondigests automatically.
.github/workflows/release.yml publishes all features under src/ to
ghcr.io/<owner>/devcontainer-features/<id> on push to main
(via devcontainers/action).
After the first publish, make the package public in the repo's Packages settings (or org package visibility) so consumer repos can pull it without auth.
npm install -g @devcontainers/cli
devcontainer features test \
--features agents \
--base-image mcr.microsoft.com/devcontainers/javascript-node:24 \
.