Tips & Tricks (Updated: 6/2/2026)

Claude Code Devcontainer Guide for Reproducible Development

Build a reproducible Claude Code devcontainer with Dockerfile, postCreateCommand, secrets, volumes, and port forwarding.

Claude Code Devcontainer Guide for Reproducible Development

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.

FilePurposeReview focus
.devcontainer/devcontainer.jsonEntry point for editor attachmentremoteUser, ports, mounts, lifecycle commands
.devcontainer/DockerfileInstalls tools and Claude CodeCLI version, non-root user, OS packages
.devcontainer/docker-compose.ymlStarts app, database, and cachevolumes, health checks, published ports
.devcontainer/post-create.shFirst-run setuplockfile handling, Prisma generation, failure behavior
.claude/settings.jsonClaude Code permission rulessecrets, 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.

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.

#Claude Code #Dev Container #VS Code #Docker #development environment
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.