Generic dev container config built on devcontainer-base. Copy this directory into your project as .devcontainer/ — no Docker build needed.
- Docker (or Docker Desktop)
- VS Code with the Dev Containers extension
- Build the base image first (one-time):
cd .devcontainer-base && ./build.sh - Pull into your project:
git clone git@github.com:peter-wagstaff/dev-container-generic.git your-project/.devcontainer
- Copy
.env.exampleto.envand set your git profile (SSH key, name, email) - Optionally add required external domains to
firewall-domains.conf - Optionally customize VS Code extensions and settings in
devcontainer.json(defaults are ESLint, Prettier, GitLens) - Open the project in VS Code and "Reopen in Container"
The git identity (SSH key, name, email) is configured via .devcontainer/.env, which is gitignored. This lets each developer use their own GitHub account without modifying tracked files.
cp .devcontainer/.env.example .devcontainer/.env
# Edit .env with your profileAvailable variables (see .env.example):
| Variable | Description |
|---|---|
GIT_SSH_KEY |
SSH key filename from your ~/.ssh directory (e.g., id_ed25519) |
GIT_USER_NAME |
Git commit author name |
GIT_USER_EMAIL |
Git commit author email |
All three are required. The container will fail to start with a clear error if any are missing.
Claude Code and Codex credentials are persisted across container rebuilds via the agent-persistence devcontainer feature. It mounts a Docker volume at /mnt/agent-persistence and symlinks ~/.claude and ~/.codex into it. Login once and credentials survive rebuilds. Shell history is also persisted via a separate per-project volume.
The feature is configured in devcontainer.json under "features":
"features": {
"ghcr.io/peter-wagstaff/devcontainer-features/agent-persistence:1": {
"scope": "per-project"
}
}| Scope | Behavior |
|---|---|
shared (default) |
One volume across all containers — all projects share credentials and config |
per-project |
Isolated subdirectory per dev container — credentials and config are separate per project |
When using per-project scope, you must also add DEVCONTAINER_ID to your containerEnv in devcontainer.json (${devcontainerId} substitution doesn't work inside feature metadata):
"containerEnv": {
"DEVCONTAINER_ID": "${devcontainerId}"
}This is already configured in devcontainer.json.
| Option | Default | What it persists |
|---|---|---|
claude |
true |
~/.claude (Claude Code config, credentials, conversation history) |
codex |
true |
~/.codex (Codex config and credentials) |
gemini |
true |
~/.gemini and related caches |
github-cli |
true |
~/.config/gh (GitHub CLI auth) |
If a project needs extra system packages, uncomment the build: block in docker-compose.yml (and comment out image:), then edit the Dockerfile:
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
<your-packages> \
&& apt-get clean && rm -rf /var/lib/apt/lists/*Add domains to firewall-domains.conf (one per line). These are merged with the base domains at container start.
Edit scripts/GLOBAL_AGENTS.md to change the global instructions installed for both Claude Code and Codex. The setup script hashes this file — changes trigger a re-install on next container create.
Plugin/skill configuration for both agents is handled in scripts/setup-dev-container.sh.
Claude Code — the setup script seeds $CLAUDE_HOME/settings.json with two plugin marketplaces and their enabled plugins:
| Plugin | Marketplace |
|---|---|
| Context7 | anthropics/claude-plugins-official |
| Superpowers | obra/superpowers-marketplace |
The settings file is only written if it doesn't already exist — once seeded, your runtime changes (enabling/disabling plugins, adding new ones) are preserved across rebuilds. To change the defaults for new containers, edit the settings.json heredoc in the setup script. To re-seed an existing container, delete $CLAUDE_HOME/settings.json and rebuild.
Codex — the setup script clones obra/superpowers into $CODEX_HOME/superpowers/ and symlinks its skills into ~/.agents/skills/superpowers/, where Codex auto-discovers them. Unlike Claude's settings, the superpowers repo is updated (git pull) on each setup run. Codex has no marketplace config — skills are found by directory scanning.
| Phase | What runs | User |
|---|---|---|
| ENTRYPOINT | firewall-entrypoint.sh → init-firewall.sh → exec sleep infinity |
root (containerUser) |
initializeCommand |
Ensures .env exists, prunes old devcontainer images |
host |
postCreateCommand |
setup-dev-container.sh + container-readiness.sh |
dev (remoteUser) |
The containerUser: root / remoteUser: dev split means the ENTRYPOINT (firewall setup) runs as root without sudo, while all developer shell sessions run as dev.
The container uses docker-compose.yml with image: devcontainer-base:latest and pull_policy: never so that the pre-built base image is used directly without a build step. Capabilities (NET_ADMIN, NET_RAW), env file, and volume mounts are declared in the compose file rather than devcontainer.json. Project-specific firewall domains are volume-mounted from firewall-domains.conf.
scripts/initialize-host.sh— Runs on the host before each build. Ensures.envexists, registers the project in~/.devcontainer-projects(for tracking orphaned volumes from deleted projects), and prunes dangling images.scripts/setup-dev-container.sh— Configures git, SSH, agent instructions, plugins, npm deps. Reads git profile from env vars (GIT_SSH_KEY,GIT_USER_NAME,GIT_USER_EMAIL). Idempotent via hash cache (re-runs when the script orGLOBAL_AGENTS.mdchanges).scripts/container-readiness.sh— Verifies required commands exist: git, make, python3, uv, node, npm, claude, codex.scripts/GLOBAL_AGENTS.md— Shared instructions installed to both$CLAUDE_HOME/CLAUDE.mdand$CODEX_HOME/AGENTS.md.