Claude Code Devcontainer Guide for Reproducible Development
Build a reproducible Claude Code devcontainer with Dockerfile, postCreateCommand, secrets, volumes, and port forwarding.
Claude Code becomes much more useful when the development environment is boring. If one teammate runs Node.js 22, another still has Node.js 20, PostgreSQL is installed differently on each laptop, and Redis is optional, Claude Code spends too much time working around environment drift. A devcontainer turns that drift into versioned repository configuration.
A Dev Container is a container-based development environment that VS Code, GitHub Codespaces, and compatible editors can attach to. In this article, we will build a practical Claude Code devcontainer for a TypeScript and Next.js project. The goal is not just “Docker works.” The goal is that Claude Code, the shell, the database client, the editor extensions, the test commands, and the forwarded ports are all reproducible.
As of June 2, 2026, the official Claude Code docs include a Development containers page with guidance on the Claude Code Dev Container Feature, credential persistence, network limits, and the risk of skipping permission prompts. Keep the VS Code Dev Containers documentation and the Dev Container Specification open as primary references when adapting this setup.
Why devcontainers fit Claude Code
Claude Code is not only a chat assistant. It can inspect files, edit code, run commands, reason over test failures, and help with review loops. That power depends on the environment being explicit. If the agent runs in an unclear host shell, it may see unrelated tools, old global packages, personal cloud credentials, or a different database than the rest of the team.
With a devcontainer, the working surface is defined in the repository. Node.js, OS packages, the Claude Code CLI, PostgreSQL clients, Redis tools, VS Code extensions, postCreateCommand, volumes, and forwarded ports can all be reviewed in code. That makes Claude Code changes easier to reproduce and easier to audit.
There are three strong use cases. First, onboarding: a new teammate opens the repository, rebuilds the container, and gets the same toolchain. Second, remote development: the same .devcontainer works locally and in Codespaces. Third, AI-assisted debugging: when Claude Code says it ran npm test, everyone knows which Node.js, dependency tree, and services were involved.
flowchart LR
Host["Host machine"] --> Editor["VS Code or compatible editor"]
Editor --> Container["Dev Container"]
Container --> Claude["Claude Code CLI"]
Container --> Tooling["Node.js / npm / psql / redis-cli"]
Container --> Services["PostgreSQL / Redis"]
Container --> Repo["Mounted repository"]
Claude --> Repo
Claude --> Tooling
The file set we will create
This example uses Next.js, PostgreSQL, and Redis because it mirrors a common product stack. You can reuse the same pattern for Astro, Express, NestJS, SvelteKit, or a content site. The important design choice is to separate the app container, service data, and Claude Code configuration.
| File | Purpose | Review focus |
|---|---|---|
.devcontainer/devcontainer.json | Entry point for editor attachment | remoteUser, ports, mounts, lifecycle commands |
.devcontainer/Dockerfile | Installs tools and Claude Code | CLI version, non-root user, OS packages |
.devcontainer/docker-compose.yml | Starts app, database, and cache | volumes, health checks, published ports |
.devcontainer/post-create.sh | First-run setup | lockfile handling, Prisma generation, failure behavior |
.claude/settings.json | Claude Code permission rules | secrets, push commands, destructive Docker commands |
Copy-ready devcontainer.json
Create .devcontainer/devcontainer.json. Keep it valid JSON; comments belong in the article or README, not in the file.
{
"name": "claude-code-next-dev",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/app",
"remoteUser": "node",
"shutdownAction": "stopCompose",
"waitFor": "postCreateCommand",
"postCreateCommand": "bash .devcontainer/post-create.sh",
"postStartCommand": "git config --global --add safe.directory /workspaces/app || true",
"forwardPorts": [3000, 5432, 6379],
"portsAttributes": {
"3000": {
"label": "Next.js",
"onAutoForward": "notify"
},
"5432": {
"label": "PostgreSQL",
"onAutoForward": "silent"
},
"6379": {
"label": "Redis",
"onAutoForward": "silent"
}
},
"mounts": [
"source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
],
"containerEnv": {
"NODE_ENV": "development",
"DISABLE_AUTOUPDATER": "1",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker",
"GitHub.copilot"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}
The remoteUser should be a non-root user. A container reduces blast radius, but the workspace is still bind-mounted from the host. If Claude Code deletes or rewrites files inside /workspaces/app, the host copy changes too. Treat permission bypass modes as a narrow tool for trusted repositories, not as a default shortcut.
The named volume for /home/node/.claude is intentional. It persists Claude Code settings and authentication data across rebuilds without mounting the host’s whole home directory. Do not share the same Claude volume across unrelated client projects. Project-scoped volumes keep credentials and history easier to reason about.
Docker Compose for services and volumes
Create .devcontainer/docker-compose.yml. The app service sleeps so the editor can attach. PostgreSQL and Redis have health checks so the first setup script does not race the services.
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
command: sleep infinity
volumes:
- ..:/workspaces/app:cached
- node_modules:/workspaces/app/node_modules
- claude_code_config:/home/node/.claude
environment:
DATABASE_URL: postgresql://app:app_password@db:5432/app
REDIS_URL: redis://redis:6379
NEXT_TELEMETRY_DISABLED: "1"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app_password
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 20
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 20
volumes:
node_modules:
claude_code_config:
postgres_data:
redis_data:
The node_modules volume avoids mixing Windows or macOS host modules with Linux container modules. The database volume keeps local data across restarts, but it can also preserve a broken migration state. Document when docker compose -f .devcontainer/docker-compose.yml down -v is acceptable, because that command deletes development data.
For ports, prefer forwardPorts before publishing Compose ports. Forwarding makes the services available to your local editor workflow without exposing database ports more broadly than needed. Only publish 5432 or 6379 when an external local tool genuinely needs it.
Pin Claude Code in the Dockerfile
The official Claude Code Dev Container Feature is convenient, but a Dockerfile gives you direct control over the Claude Code CLI version. On June 2, 2026, npm view @anthropic-ai/claude-code version returned 2.1.160, so this example pins that version. Check the current official docs and npm package before adopting the number in a long-lived team repository.
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
ARG CLAUDE_CODE_VERSION=2.1.160
ENV DISABLE_AUTOUPDATER=1
ENV NEXT_TELEMETRY_DISABLED=1
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
jq \
postgresql-client \
redis-tools \
ripgrep \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
&& npm cache clean --force
USER node
WORKDIR /workspaces/app
RUN mkdir -p /home/node/.claude
Pinning does not mean you never update. It means updates become reviewable. Bump the ARG, rebuild, run claude --version, run linting and tests, and record the result. That is easier to trust than a container that silently changes behavior after every rebuild.
Use postCreateCommand for repeatable setup
Put first-run setup in .devcontainer/post-create.sh. A script is easier to review than a long one-line JSON command.
#!/usr/bin/env bash
set -euo pipefail
cd /workspaces/app
echo "==> enabling corepack"
corepack enable
echo "==> installing dependencies"
if [ -f pnpm-lock.yaml ]; then
pnpm install --frozen-lockfile
elif [ -f yarn.lock ]; then
yarn install --immutable
elif [ -f package-lock.json ]; then
npm ci
elif [ -f package.json ]; then
npm install
else
echo "No package.json found. Skipping dependency install."
fi
if [ -f prisma/schema.prisma ]; then
echo "==> generating Prisma client"
npx prisma generate
fi
echo "==> versions"
node --version
npm --version
claude --version || true
The script preserves the package manager chosen by the repository. A common failure is asking Claude Code to “install dependencies” and accidentally replacing a pnpm or Yarn workflow with plain npm install. Another failure is hiding setup errors. set -euo pipefail makes the container creation stop when the setup is actually broken.
Do not start npm run dev from postCreateCommand. Long-running application servers belong in a manual terminal command, a VS Code task, or a separate service. postCreateCommand should finish.
Permissions and secrets
A devcontainer is not a reason to mount every host secret. Avoid mounting ~/.ssh, ~/.aws, ~/.config/gcloud, production .env files, and customer credentials unless the project truly requires them and the risk is reviewed. The safest secret is the one not passed into the container.
For project-level Claude Code rules, create .claude/settings.json.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run test *)",
"Bash(npm run typecheck)",
"Bash(node --version)",
"Bash(claude --version)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(printenv *)",
"Bash(git push *)",
"Bash(docker system prune *)"
]
}
}
This is not a replacement for human review. If npm run test itself prints secrets, the permission rule will not magically make the script safe. Review package.json, keep production credentials out of the container, and use managed settings for stricter organization-wide policy when needed.
Practical Claude Code prompts
Use explicit prompts that describe boundaries:
Add a Claude Code devcontainer for this repository.
Requirements:
- Use a Node.js 22 Dev Container image.
- Pin the Claude Code CLI in Dockerfile.
- Use docker compose for app, PostgreSQL, and Redis.
- Use remoteUser node.
- Store ~/.claude in a project-scoped named volume.
- Deny .env, secrets, git push, and destructive Docker commands in Claude Code permissions.
- Put postCreateCommand logic in a bash script that respects lockfiles.
- Review ports, volumes, secrets, and lifecycle commands after the change.
Then ask for a review-only pass:
Do not edit files. Review only the devcontainer setup.
Check reproducibility, non-root execution, secret exposure, named volumes,
port forwarding, package manager handling, and postCreateCommand failure behavior.
Return risks with file paths and suggested fixes.
Failure cases to avoid
The first failure is running Claude Code as root. That can create root-owned files in the workspace and makes permission bypass modes riskier. Keep remoteUser and the Dockerfile USER aligned.
The second failure is letting the CLI auto-update in a team environment. A fresh rebuild can behave differently from yesterday’s rebuild. Pin the version, then update intentionally.
The third failure is forwarding or publishing too much. 3000 is usually useful. 5432 and 6379 should be silent internal service ports unless you have a local database GUI that needs them.
The fourth failure is trusting .claude/settings.json while still mounting production secrets. Permission rules reduce risk, but not passing the secret is stronger.
The fifth failure is ignoring persistent volumes. They improve speed, but they also preserve broken database state. Make volume reset instructions part of the README.
Verification and related reading
After rebuilding the container, run node --version, npm --version, claude --version, at least one lint or test command, and a browser check for http://localhost:3000. Confirm that .env, .env.local, and secrets/** are not readable through Claude Code. Check that the database and Redis are not unnecessarily published through Compose ports.
For deeper service setup, read the Claude Code Docker Compose guide. For turning this into a release workflow, pair it with the Claude Code CI/CD setup guide. For teams that want this as a repeatable operating practice, ClaudeCodeLab’s training and consultation can help turn devcontainers, CLAUDE.md, permissions, reviews, and article quality checks into a shared workflow.
What happened when I tried this
In a small Next.js test repository, the biggest improvement came from separating postCreateCommand, node_modules, and /home/node/.claude. Rebuilding the container gave the same Claude Code version, dependency install path, Prisma generation behavior, and port-forwarding behavior each time. The main issue was not Dockerfile syntax; it was old database volume state after repeated migration experiments. The practical lesson is that Claude Code devcontainer reproducibility depends on version pinning, secret boundaries, volume reset rules, and port-forwarding discipline together.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.