Skip to main content

Claude Code Dev Containers

by George Liu

Dev Containers Guide

Your complete beginner-friendly guide to development containers with Claude Code

⚡ Fast Setup 🔒 Isolated Environment 🔄 Reproducible

👋 Welcome!

This guide will teach you everything you need to know about development containers (devcontainers) for Claude Code. Whether you're a complete beginner or looking to refine your setup, you're in the right place.

📚 What You'll Learn:

  • What dev containers are and how they work
  • Setting up a React project with dev containers
  • Setting up a Python project with dev containers
  • Setting up a Next.js application with dev containers
  • Configuring multiple AI assistants in one container
  • Troubleshooting common issues

💡 Pro Tip for AI Users:

You can provide this comprehensive llms.txt guide to AI assistants like Claude to help with dev container creation and usage. It contains detailed examples and best practices!

🤔 What Are Development Containers?

Think of a development container as a complete development workspace in a box. It's like having a fresh computer dedicated just for your project, but it runs inside your current computer!

💡 Real-World Analogy

Imagine you're a chef. Instead of installing every cooking tool and ingredient in your home kitchen (which gets messy and cluttered), a dev container is like having a fully-equipped portable kitchen that appears ready to go whenever you need it, then disappears when you're done. Each recipe (project) gets its own kitchen setup!

How It Works

┌─────────────────────────────────────────┐
│    Your Computer (Mac/Windows/Linux)   │
│                                         │
│  ┌───────────────────────────────────┐ │
│  │         VS Code                   │ │
│  │  (Your Editor on the outside)     │ │
│  └───────────────â”Ŧ───────────────────┘ │
│                  │                     │
│                  │ Connects to         │
│                  â–ŧ                     │
│  ┌───────────────────────────────────┐ │
│  │    đŸŗ Docker Container            │ │
│  │    (Isolated workspace)           │ │
│  │                                   │ │
│  │  ✅ Node.js 20                    │ │
│  │  ✅ Python 3.11                   │ │
│  │  ✅ Git, Claude Code, tools       │ │
│  │  ✅ Your project files            │ │
│  └───────────────────────────────────┘ │
└─────────────────────────────────────────┘

Key Benefits

Zero Setup

No manual installation of Node.js, Python, or tools. It's all pre-installed!

Isolated

Each project runs in its own container. No conflicts between projects!

Reproducible

Same setup on every computer. "Works on my machine" problems disappear!

Disposable

Delete and rebuild anytime without losing your configuration files.

✅ Why Use Development Containers?

For Individual Developers

  • Clean Host System: Your computer stays clean - no installing dozens of tools globally
  • Multiple Environments: Work on Python 3.9 and Python 3.11 projects simultaneously without conflicts
  • Consistency: Same setup works perfectly on Mac, Windows, and Linux

For Teams

  • Fast Onboarding: New team members coding in minutes instead of hours/days
  • Standardization: Everyone uses the exact same tool versions
  • Documentation as Code: Setup is version-controlled and documented automatically

âš ī¸ When NOT to Use Dev Containers

Dev containers aren't always the best solution. Here's when to skip them:

  • Simple Scripts: For a single Python script or small Node.js file, just run it locally
  • GUI Applications: Desktop apps requiring native UI don't work well in containers
  • Hardware Access: USB devices, GPUs, specialized hardware need complex setup
  • Resource-Constrained Machines: Docker overhead (1-2GB RAM) may be too much for older computers

Better alternatives: For simple projects, use nvm, pyenv, or rbenv for version management.

📋 Prerequisites

Before creating your first dev container, you'll need to install:

1. VS Code

The editor that will connect to your containers.

Download VS Code →

2. Docker Desktop

The engine that runs containers.

Download Docker Desktop →
Note: Windows users need WSL2 enabled first.

3. Dev Containers Extension

VS Code extension to manage dev containers.

Install from VS Code: Press Cmd+Shift+X (Mac) or Ctrl+Shift+X (Windows/Linux), search "Dev Containers", and click Install.

✓ Verify Installation

Run these commands in your terminal:

# Check Docker is running
docker --version
docker ps

# Check VS Code is installed
code --version

đŸ—ī¸ Architecture Overview

A dev container consists of three main components working together:

📄 devcontainer.json

The configuration file that tells VS Code how to connect to your container.

  • Defines which extensions to install
  • Sets environment variables
  • Configures port forwarding
  • Specifies post-creation commands

đŸŗ Dockerfile

The blueprint that defines what's installed inside your container.

  • Specifies base image (e.g., Node.js 20, Python 3.11)
  • Installs system packages and tools
  • Sets up users and permissions
  • Configures the shell environment

âš™ī¸ Init Scripts (Optional)

Automation scripts that run after the container starts.

  • Initialize tool configurations
  • Set up security policies
  • Prepare the development environment

📁 File Structure Example

your-project/
├── .devcontainer/
│   ├── devcontainer.json       # VS Code configuration
│   ├── Dockerfile              # Container blueprint
│   ├── init-claude-config.sh   # Claude Code auto-config
│   ├── init-codex-config.sh    # Codex CLI auto-config
│   ├── init-firewall.sh        # Network security
│   ├── settings.json.template  # Claude settings
│   ├── mcp.json.template       # MCP servers
│   └── config.toml.template    # Codex configuration
├── .gitignore
├── README.md
└── [your project files]

🔒 Security & Firewall Setup

Production dev containers should implement network security with iptables firewall configuration. This creates a default-deny security posture where only explicitly allowed domains can be accessed.

âš ī¸ Security Architecture:

  • Default-deny policy: All outbound traffic blocked by default
  • Allowlist-based: Only whitelisted domains (npm, GitHub, AI APIs) permitted
  • IPv6 disabled: Prevents bypassing IPv4 firewall rules
  • Docker-aware: Preserves inter-container communication
  • Startup verification: Validates rules and tests connectivity

📚 Why Firewall for Dev Containers?

According to Claude's official documentation on sandboxing, network isolation is a core security layer. The firewall provides:

  • Protection against data exfiltration to untrusted domains
  • Controlled access to package registries and AI APIs
  • Defense-in-depth alongside other security measures
  • Audit trail of allowed network destinations

Implementation Components

1. Docker Capabilities in devcontainer.json

Add these runArgs to grant minimal capabilities needed for iptables management:

"runArgs": [
  "--cap-drop=ALL",           // Drop all capabilities first
  "--cap-add=NET_ADMIN",      // Required for iptables management
  "--cap-add=NET_RAW",        // Required for packet filtering
  "--cap-add=SETUID",         // Required for sudo operations
  "--cap-add=SETGID"          // Required for group switching
],
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh"

2. init-firewall.sh Script

Create this script in your .devcontainer/ directory. It will be copied to /usr/local/bin/ during container build:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

STATE_FILE="/var/lib/firewall-configured"

# Disable IPv6 to prevent bypass
echo "Disabling IPv6..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.lo.disable_ipv6=1 >/dev/null 2>&1 || true

# Check if already configured
if [ -f "$STATE_FILE" ]; then
    echo "Firewall already configured, skipping..."
    exit 0
fi

echo "Starting firewall configuration..."

# Extract Docker DNS before flushing
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)

# Detect Docker networks
echo "Detecting Docker networks..."
DOCKER_NETWORKS=$(ip -o -f inet addr show | grep -v "127.0.0.1" | awk '{print $4}')

if [ -z "$DOCKER_NETWORKS" ]; then
    echo "ERROR: Failed to detect Docker networks"
    exit 1
fi

# Create ipset for allowed domains
ipset destroy allowed-domains 2>/dev/null || true
ipset create allowed-domains hash:net

# Fetch GitHub IP ranges
echo "Fetching GitHub IP ranges..."
gh_ranges=$(curl -s https://api.github.com/meta)
if [ -z "$gh_ranges" ]; then
    echo "ERROR: Failed to fetch GitHub IP ranges"
    exit 1
fi

echo "Processing GitHub IPs..."
while read -r cidr; do
    echo "Adding GitHub range $cidr"
    ipset add allowed-domains "$cidr" -exist
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)

# Resolve and add allowed domains
for domain in \
    "dns.google" \
    "1.1.1.1" \
    "8.8.8.8" \
    "8.8.4.4" \
    "auth.openai.com" \
    "chatgpt.com" \
    "context7.com" \
    "unpkg.com" \
    "cdn.jsdelivr.net" \
    "cdnjs.cloudflare.com" \
    "github.com" \
    "api.github.com" \
    "raw.githubusercontent.com" \
    "github.githubassets.com" \
    "collector.github.com" \
    "ghcr.io" \
    "pkg-containers.githubusercontent.com" \
    "nodejs.org" \
    "registry.npmjs.org" \
    "pypi.org" \
    "files.pythonhosted.org" \
    "astral.sh" \
    "bun.sh" \
    "crates.io" \
    "static.crates.io" \
    "index.crates.io" \
    "docker.io" \
    "registry-1.docker.io" \
    "auth.docker.io" \
    "production.cloudflare.docker.com" \
    "api.anthropic.com" \
    "api.openai.com" \
    "aistudio.google.com" \
    "accounts.google.com" \
    "oauth2.googleapis.com" \
    "www.googleapis.com" \
    "storage.googleapis.com" \
    "content.googleapis.com" \
    "generativelanguage.googleapis.com" \
    "sentry.io" \
    "statsig.anthropic.com" \
    "statsig.com" \
    "marketplace.visualstudio.com" \
    "vscode.blob.core.windows.net" \
    "update.code.visualstudio.com" \
    "docs.mcp.cloudflare.com" \
    "mcp.context7.com" \
    "vercel.com" \
    "ui.shadcn.com" \
    "tailwindcss.com" \
    "radix-ui.com" \
    "fonts.googleapis.com" \
    "fonts.gstatic.com" \
    "react.dev" \
    "reactjs.org" \
    "esm.sh" \
    "lucide.dev"; do

    # Check if IP address
    if [[ "$domain" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        echo "Adding IP $domain"
        ipset add allowed-domains "$domain" -exist
        continue
    fi

    # Resolve hostname
    echo "Resolving $domain..."
    ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
    if [ -z "$ips" ]; then
        echo "ERROR: Failed to resolve $domain"
        exit 1
    fi

    while read -r ip; do
        echo "Adding $ip for $domain"
        ipset add allowed-domains "$ip" -exist
    done < <(echo "$ips")
done

echo "IP allowlist built successfully"

# Flush iptables
echo "Flushing iptables..."
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X

# Restore Docker DNS
if [ -n "$DOCKER_DNS_RULES" ]; then
    echo "Restoring Docker DNS..."
    iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
    iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
    echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
fi

# Configure base rules
echo "Configuring base rules..."
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Allow Docker networks
echo "Allowing Docker networks..."
while read -r network; do
    echo "  Allowing: $network"
    iptables -A INPUT -s "$network" -j ACCEPT
    iptables -A OUTPUT -d "$network" -j ACCEPT
done < <(echo "$DOCKER_NETWORKS")

# Set default-deny policies
echo "Setting default-deny policies..."
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow whitelisted domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT

# Reject all other traffic
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited

echo ""
echo "Firewall configuration complete!"
echo ""

# Verify firewall
echo "Verifying firewall..."

# Check IPv6 disabled
if sysctl net.ipv6.conf.all.disable_ipv6 | grep -q "= 1"; then
    echo "✓ IPv6 disabled"
else
    echo "✗ IPv6 still enabled"
    exit 1
fi

# Check blocked domains
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
    echo "✗ Firewall failed - can reach example.com"
    exit 1
else
    echo "✓ Blocked example.com as expected"
fi

# Check allowed domains
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
    echo "✗ Cannot reach api.github.com"
    exit 1
else
    echo "✓ Can reach api.github.com as expected"
fi

# Mark as configured
touch "$STATE_FILE"
echo "Firewall configured successfully!"

🔑 Key Features:

  • Idempotent: Uses state file to skip if already configured
  • GitHub IP aggregation: Fetches and aggregates IP ranges from GitHub's API
  • DNS resolution: Resolves all allowed domains to IP addresses at startup
  • Docker preservation: Maintains inter-container communication
  • Self-testing: Verifies both blocked and allowed domains

3. Dockerfile Integration

Add these lines to your Dockerfile to install the script and configure sudo:

# Copy firewall script
COPY init-firewall.sh /usr/local/bin/

# Set permissions and configure sudoers
RUN chmod +x /usr/local/bin/init-firewall.sh && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
    chmod 0440 /etc/sudoers.d/node-firewall

đŸ›Ąī¸ Security Considerations:

  • Not foolproof: Firewall provides defense-in-depth but is not a complete security solution
  • Trusted repos only: Only use dev containers with repositories you trust
  • Bypass flag: --dangerously-skip-permissions bypasses security checks - use with caution
  • Customization: Add your project's specific domains to the allowlist as needed
  • Performance: One-time ~10-30 second startup overhead for DNS resolution and rule configuration

âš™ī¸ Claude Code Configuration & MCP

Automatically configure Claude Code settings and MCP (Model Context Protocol) servers on container startup. This provides enhanced functionality like library documentation lookup and Cloudflare-specific help.

💡 What is MCP?

Model Context Protocol (MCP) extends Claude Code with specialized capabilities through remote servers. Think of it like installing plugins that give Claude instant access to documentation and APIs.

  • context7: Look up up-to-date library documentation (React, Next.js, Supabase, MongoDB, etc.)
  • cf-docs: Search Cloudflare documentation (Workers, Pages, R2, D1, etc.)

The init-claude-config.sh Script

This script automatically sets up Claude Code configuration on container startup by:

  • Creating .claude directory if it doesn't exist
  • Copying MCP server configuration for context7 and cf-docs
  • Copying settings template with optimized environment variables
  • Ensuring npm global directory structure exists (required for npx with MCP)
  • Preserving user settings if they already exist (won't overwrite)

1. Create init-claude-config.sh

Save this script in your .devcontainer directory:

#!/bin/bash
set -euo pipefail

CLAUDE_HOME="/home/node/.claude"
MCP_FILE="$CLAUDE_HOME/mcp.json"
MCP_TEMPLATE="/usr/local/share/claude-defaults/mcp.json"
SETTINGS_TEMPLATE="/usr/local/share/claude-defaults/settings.json"

echo "Initializing Claude Code configuration..."

# Create .claude directory if it doesn't exist
if [ ! -d "$CLAUDE_HOME" ]; then
    echo "Creating $CLAUDE_HOME directory..."
    mkdir -p "$CLAUDE_HOME"
    chown node:node "$CLAUDE_HOME"
fi

# Copy MCP configuration if it doesn't exist
if [ ! -f "$MCP_FILE" ]; then
    if [ -f "$MCP_TEMPLATE" ]; then
        echo "Copying MCP server configuration..."
        cp "$MCP_TEMPLATE" "$MCP_FILE"
        chown node:node "$MCP_FILE"
        echo "✓ MCP servers configured:"
        echo "  - context7 (https://mcp.context7.com/sse)"
        echo "  - cf-docs (https://docs.mcp.cloudflare.com/sse)"
    else
        echo "Warning: MCP template not found at $MCP_TEMPLATE"
    fi
else
    echo "MCP configuration already exists, preserving user settings"
fi

# Copy settings.json template if it doesn't exist
if [ ! -f "$CLAUDE_HOME/settings.json" ]; then
    if [ -f "$SETTINGS_TEMPLATE" ]; then
        echo "Copying Claude Code settings from template..."
        cp "$SETTINGS_TEMPLATE" "$CLAUDE_HOME/settings.json"
        chown node:node "$CLAUDE_HOME/settings.json"
        echo "✓ Environment variables configured (MAX_MCP_OUTPUT_TOKENS, timeouts)"
    fi
else
    echo "Settings already exist, preserving user settings"
fi

# Ensure npm global directory structure exists (required for npx with MCP servers)
if [ ! -d "/home/node/.npm-global/lib" ]; then
    echo "Creating npm global directory structure..."
    mkdir -p /home/node/.npm-global/lib
    chown -R node:node /home/node/.npm-global
    echo "✓ npm global directory initialized"
fi

echo "Claude Code configuration complete"

2. Create settings.json.template

This template configures Claude Code with optimized settings:

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "dangerously_skip_permissions": true,
  "verbose": true,
  "env": {
    "MAX_MCP_OUTPUT_TOKENS": "60000",
    "BASH_DEFAULT_TIMEOUT_MS": "300000",
    "BASH_MAX_TIMEOUT_MS": "600000",
    "MAX_THINKING_TOKENS": "8192",
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOG_USER_PROMPTS": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://otel-collector:4317",
    "OTEL_RESOURCE_ATTRIBUTES": "deployment.environment=devcontainer,service.name=claude-code"
  },
  "includeCoAuthoredBy": false,
  "mcpServers": {
    "context7": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.context7.com/mcp"],
      "transport": {
        "type": "stdio"
      }
    },
    "cf-docs": {
      "command": "npx",
      "args": ["mcp-remote", "https://docs.mcp.cloudflare.com/mcp"],
      "transport": {
        "type": "stdio"
      }
    }
  }
}

🔧 Key Settings Explained:

  • MAX_MCP_OUTPUT_TOKENS: Allows larger documentation responses (60,000 tokens)
  • BASH_*_TIMEOUT_MS: Extended timeouts for long-running commands
  • MAX_THINKING_TOKENS: Enables deeper reasoning (8,192 tokens)
  • OTEL_* variables: OpenTelemetry configuration for observability (optional)

3. Create mcp.json.template

Alternative MCP configuration using SSE (Server-Sent Events) transport:

{
  "mcpServers": {
    "context7": {
      "transport": {
        "type": "sse",
        "url": "https://mcp.context7.com/sse"
      }
    },
    "cf-docs": {
      "transport": {
        "type": "sse",
        "url": "https://docs.mcp.cloudflare.com/sse"
      }
    }
  }
}

4. Create config.toml.template (Codex CLI)

This template configures Codex CLI for Docker containers by disabling the Landlock sandbox (not supported in LinuxKit kernel):

# Codex CLI configuration template for Docker containers
# Auto-copied to ~/.codex/config.toml by init-codex-config.sh

model = "gpt-5"
model_reasoning_effort = "medium"

# CRITICAL: Disable Landlock sandbox (not supported in Docker's LinuxKit kernel)
# OpenAI's official recommendation for containerized environments
# Security is provided by Docker isolation, network firewall, and non-root user
sandbox_mode = "danger-full-access"
approval_policy = "never"

🔒 Why Disable Sandbox?

  • Docker's LinuxKit kernel doesn't support Landlock security module
  • Container already provides isolation via Docker, firewall, non-root user
  • OpenAI's official recommendation for containerized environments
  • See Troubleshooting → Codex CLI Landlock Error for details

5. Create init-codex-config.sh

This script automatically configures Codex CLI on container startup:

#!/bin/bash
set -euo pipefail

CODEX_HOME="/home/node/.codex"
CONFIG_FILE="$CODEX_HOME/config.toml"
CONFIG_TEMPLATE="/usr/local/share/codex-defaults/config.toml"

echo "Initializing Codex CLI configuration..."

# Create .codex directory if it doesn't exist
if [ ! -d "$CODEX_HOME" ]; then
    echo "Creating $CODEX_HOME directory..."
    mkdir -p "$CODEX_HOME"
    chown node:node "$CODEX_HOME"
fi

# Copy config template if it doesn't exist
if [ ! -f "$CONFIG_FILE" ]; then
    if [ -f "$CONFIG_TEMPLATE" ]; then
        echo "Copying Codex CLI configuration from template..."
        cp "$CONFIG_TEMPLATE" "$CONFIG_FILE"
        chown node:node "$CONFIG_FILE"
        echo "✓ Codex sandbox disabled (Docker container isolation used instead)"
        echo "  sandbox_mode: danger-full-access"
        echo "  approval_policy: never"
    else
        echo "Warning: Codex config template not found at $CONFIG_TEMPLATE"
    fi
else
    echo "Codex config already exists, preserving user settings"
fi

echo "Codex CLI configuration complete"

💡 Key Features:

  • Idempotent: Only creates config if it doesn't exist
  • Preserves user settings: Won't overwrite existing configuration
  • Proper ownership: Uses chown to ensure node user can access files
  • Informative output: Logs what it's doing for debugging

6. Integrate into Dockerfile

Add these sections to your Dockerfile to enable automatic configuration:

# Switch to root for setup
USER root

# Create directories for Claude Code and Codex defaults
RUN mkdir -p /usr/local/share/claude-defaults /usr/local/share/codex-defaults

# Copy init scripts and templates
COPY init-claude-config.sh /usr/local/bin/
COPY init-codex-config.sh /usr/local/bin/
COPY --chown=node:node settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=node:node mcp.json.template /usr/local/share/claude-defaults/mcp.json
COPY --chown=node:node config.toml.template /usr/local/share/codex-defaults/config.toml

# Set permissions and configure sudoers for all init scripts
RUN chmod +x /usr/local/bin/init-claude-config.sh && \
    chmod +x /usr/local/bin/init-codex-config.sh && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/node-claude-config && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-codex-config.sh" > /etc/sudoers.d/node-codex-config && \
    chmod 0440 /etc/sudoers.d/node-claude-config && \
    chmod 0440 /etc/sudoers.d/node-codex-config

# Switch back to node user
USER node

7. Update devcontainer.json

Run all initialization scripts on container startup:

{
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-codex-config.sh && sudo /usr/local/bin/init-firewall.sh"
}

📝 Init Script Order:

  1. init-claude-config.sh - Configure Claude Code & MCP servers
  2. init-codex-config.sh - Configure Codex CLI (disable Landlock sandbox)
  3. init-firewall.sh - Apply network security rules (runs last)

Config scripts run before the firewall to ensure npm/npx can download packages if needed. All scripts run automatically each time the container starts.

✅ Benefits:

  • Zero manual setup: MCP servers configured automatically on first run
  • Instant documentation access: Ask Claude about any library and get current docs
  • Preserves customization: Won't overwrite if you modify settings manually
  • Team consistency: Everyone gets the same optimized configuration
  • Extensible: Easy to add more MCP servers or environment variables

âš›ī¸ React Project Setup

Let's create a dev container for a React project. Follow these steps:

Create the .devcontainer directory

mkdir -p .devcontainer
cd .devcontainer

Create devcontainer.json

This file configures VS Code's connection to the container:

{
  "name": "React Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      }
    }
  },
  "runArgs": [
    "--cap-drop=ALL",
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW",
    "--cap-add=SETUID",
    "--cap-add=SETGID"
  ],
  "forwardPorts": [3000, 5173],
  "postCreateCommand": "npm install",
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-firewall.sh",
  "remoteUser": "node",
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume"
  ]
}

💡 What's happening here:

  • extensions: Auto-installs Claude Code, ESLint, Prettier, and Tailwind CSS support
  • runArgs: Grants minimal Docker capabilities for iptables firewall management (see Security & Firewall Setup)
  • forwardPorts: Makes ports 3000 and 5173 accessible on your host machine
  • postCreateCommand: Runs npm install automatically after container creation
  • postStartCommand: Configures network firewall on container startup
  • mounts: Persists Claude Code settings between container rebuilds

Create Dockerfile

This defines what gets installed in your container:

# Start from Node.js 20 base image
FROM node:20

# Install essential tools and firewall dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    iptables \
    ipset \
    dnsutils \
    aggregate \
    jq \
    sudo \
    && rm -rf /var/lib/apt/lists/*

# Install Claude Code CLI globally
RUN npm install -g @anthropic-ai/claude-code@latest

# Create directory for Claude Code defaults
RUN mkdir -p /usr/local/share/claude-defaults

# Copy initialization scripts and templates
COPY init-firewall.sh /usr/local/bin/
COPY init-claude-config.sh /usr/local/bin/
COPY --chown=node:node settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=node:node mcp.json.template /usr/local/share/claude-defaults/mcp.json

# Set permissions and configure sudoers for both scripts
RUN chmod +x /usr/local/bin/init-firewall.sh && \
    chmod +x /usr/local/bin/init-claude-config.sh && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/node-claude-config && \
    chmod 0440 /etc/sudoers.d/node-firewall && \
    chmod 0440 /etc/sudoers.d/node-claude-config

# Set working directory
WORKDIR /workspace

# Configure git (optional, for better commit messages)
RUN git config --global init.defaultBranch main

# Switch to non-root user for security
USER node

✅ What each line does:

  • FROM node:20: Uses official Node.js 20 as the base
  • RUN apt-get: Installs Git, curl, and firewall tools (iptables, ipset, dnsutils, aggregate, jq, sudo)
  • npm install -g: Installs Claude Code globally in the container
  • COPY init-firewall.sh: Installs the firewall configuration script (see Security & Firewall Setup)
  • sudoers configuration: Allows node user to run firewall script without password
  • USER node: Runs as non-root user for better security

📝 Note: init-firewall.sh Script

Create the init-firewall.sh script in your .devcontainer/ directory. The complete script with all allowed domains is provided in the Security & Firewall Setup section above.

Open in VS Code

Now open your project in VS Code. You'll see a prompt:

"Folder contains a Dev Container configuration file. Reopen folder to develop in a container?"

Click "Reopen in Container". VS Code will:

  1. Build the Docker image (takes 5-10 minutes first time)
  2. Start the container
  3. Install VS Code extensions
  4. Run npm install
  5. Connect VS Code to the running container

âąī¸ First Build: The first time takes longer (5-10 minutes) because Docker downloads the base image and installs everything. Subsequent starts are much faster (~30 seconds).

Test Your Setup

Once connected, open the integrated terminal and verify:

# Check Node.js version
node --version  # Should show v20.x.x

# Check npm version
npm --version

# Check Claude Code is installed
claude --version

# Start your React dev server
npm run dev  # or npm start

🎉 Your React app should now be running at http://localhost:3000 or http://localhost:5173 (Vite)!

🐍 Basic Python Project Setup

Let's create a simple dev container for a Python project with Flask or FastAPI. This basic setup is perfect for learning, simple scripts, and lightweight projects.

📌 Basic vs Advanced:

This basic setup uses standard pip and minimal dependencies. For production projects with advanced tooling (uv package manager, bun for hybrid apps, extended libraries), see the Advanced Python Setup below.

Create devcontainer.json for Python

{
  "name": "Python Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-python.black-formatter",
        "charliermarsh.ruff"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "python.linting.enabled": true,
        "python.formatting.provider": "black",
        "editor.formatOnSave": true
      }
    }
  },
  "runArgs": [
    "--cap-drop=ALL",
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW",
    "--cap-add=SETUID",
    "--cap-add=SETGID"
  ],
  "forwardPorts": [5000, 8000],
  "postCreateCommand": "pip install -r requirements.txt",
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-firewall.sh",
  "remoteUser": "vscode",
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/vscode/.claude,type=volume"
  ]
}

Create Dockerfile for Python

# Start from Python 3.11 slim image (smaller size)
FROM python:3.11-slim

# Install system dependencies and firewall tools
RUN apt-get update && apt-get install -y \
    git \
    curl \
    build-essential \
    iptables \
    ipset \
    dnsutils \
    aggregate \
    jq \
    sudo \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN useradd -ms /bin/bash vscode

# Install Claude Code CLI
RUN curl -fsSL https://claude.ai/install.sh | bash

# Create directory for Claude Code defaults
RUN mkdir -p /usr/local/share/claude-defaults

# Copy initialization scripts and templates
COPY init-firewall.sh /usr/local/bin/
COPY init-claude-config.sh /usr/local/bin/
COPY --chown=vscode:vscode settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=vscode:vscode mcp.json.template /usr/local/share/claude-defaults/mcp.json

# Set permissions and configure sudoers for both scripts
RUN chmod +x /usr/local/bin/init-firewall.sh && \
    chmod +x /usr/local/bin/init-claude-config.sh && \
    echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/vscode-firewall && \
    echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/vscode-claude-config && \
    chmod 0440 /etc/sudoers.d/vscode-firewall && \
    chmod 0440 /etc/sudoers.d/vscode-claude-config

# Set working directory
WORKDIR /workspace

# Install Python dev tools globally
RUN pip install --no-cache-dir \
    black \
    pylint \
    pytest \
    ipython

# Switch to non-root user
USER vscode

# Set Python to unbuffered mode (better for logs)
ENV PYTHONUNBUFFERED=1

🔍 Key differences from React setup:

  • python:3.11-slim: Smaller base image compared to full Python
  • build-essential: Needed for packages that compile C extensions
  • pip install: Pre-installs common Python dev tools
  • PYTHONUNBUFFERED=1: Makes print() output appear immediately in logs

Create requirements.txt

List your Python dependencies (example for Flask):

flask==3.0.0
python-dotenv==1.0.0
requests==2.31.0

Or for FastAPI:

fastapi==0.104.0
uvicorn[standard]==0.24.0
python-dotenv==1.0.0
pydantic==2.5.0

Test Your Python Setup

After reopening in container, test in the terminal:

# Check Python version
python --version  # Should show Python 3.11.x

# Check pip version
pip --version

# Verify dependencies installed
pip list

# Run Flask app (example)
python app.py

# Or run FastAPI app (example)
uvicorn main:app --reload --host 0.0.0.0 --port 8000

🎉 Your Python app should now be accessible at http://localhost:5000 (Flask) or http://localhost:8000 (FastAPI)!

⚡ Advanced Python Project Setup

A production-ready Python dev container with modern tooling, extended libraries, and hybrid app support (Python + JavaScript frontends).

⚡ What's Included:

  • uv: Fast Python package manager (10-100x faster than pip) + version management
  • bun: JavaScript runtime for hybrid Python/JS projects (React, Vue, Svelte frontends)
  • Extended libraries: Development libraries for data science, web scraping, and native extensions
  • Modern tools: ripgrep, fd-find, jq, yq for faster development

Create devcontainer.json

{
  "name": "Advanced Python Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-python.black-formatter",
        "charliermarsh.ruff"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "python.linting.enabled": true,
        "python.formatting.provider": "black",
        "editor.formatOnSave": true
      }
    }
  },
  "runArgs": [
    "--cap-drop=ALL",
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW",
    "--cap-add=SETUID",
    "--cap-add=SETGID"
  ],
  "forwardPorts": [5000, 8000, 8888],
  "postCreateCommand": "uv pip install -r requirements.txt",
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-firewall.sh",
  "remoteUser": "vscode",
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
    "source=cargo-${devcontainerId},target=/home/vscode/.cargo,type=volume",
    "source=bun-${devcontainerId},target=/home/vscode/.bun,type=volume"
  ]
}

💡 What's different:

  • Port 8888: Added for Jupyter Lab/Notebook
  • uv pip install: Uses uv for 10-100x faster package installation
  • cargo volume: Persists uv installation and Python versions
  • bun volume: Persists bun installation for hybrid projects

Create Dockerfile (Advanced)

# Start from Python 3.11 slim image
FROM python:3.11-slim

# Install extended system dependencies (organized by category)
RUN apt-get update && apt-get install -y \
    # Version control
    git \
    git-lfs \
    # Core utilities
    curl \
    wget \
    ca-certificates \
    # Build tools for native extensions
    build-essential \
    gcc \
    g++ \
    make \
    cmake \
    pkg-config \
    # Development libraries (for data science, web scraping, crypto)
    libffi-dev \
    libssl-dev \
    libz-dev \
    libbz2-dev \
    libreadline-dev \
    libsqlite3-dev \
    libncurses5-dev \
    libncursesw5-dev \
    liblzma-dev \
    # Text processing and data tools
    jq \
    yq \
    sed \
    gawk \
    # Modern search tools (faster alternatives)
    ripgrep \
    fd-find \
    # System utilities
    htop \
    tree \
    procps \
    # Network tools
    net-tools \
    iputils-ping \
    dnsutils \
    # Firewall tools
    iptables \
    ipset \
    aggregate \
    sudo \
    # Text editors
    vim \
    nano \
    # Locales for proper encoding
    locales \
    && rm -rf /var/lib/apt/lists/*

# Set up locale (prevents encoding issues)
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
    locale-gen
ENV LANG=en_US.UTF-8 \
    LANGUAGE=en_US:en \
    LC_ALL=en_US.UTF-8

# Create non-root user
RUN useradd -ms /bin/bash vscode

# Install Claude Code CLI
RUN curl -fsSL https://claude.ai/install.sh | bash

# Create directory for Claude Code defaults
RUN mkdir -p /usr/local/share/claude-defaults

# Copy initialization scripts and templates
COPY init-firewall.sh /usr/local/bin/
COPY init-claude-config.sh /usr/local/bin/
COPY --chown=vscode:vscode settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=vscode:vscode mcp.json.template /usr/local/share/claude-defaults/mcp.json

# Set permissions and configure sudoers for both scripts
RUN chmod +x /usr/local/bin/init-firewall.sh && \
    chmod +x /usr/local/bin/init-claude-config.sh && \
    echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/vscode-firewall && \
    echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/vscode-claude-config && \
    chmod 0440 /etc/sudoers.d/vscode-firewall && \
    chmod 0440 /etc/sudoers.d/vscode-claude-config

# Set working directory
WORKDIR /workspace

# Install Python dev tools globally
RUN pip install --no-cache-dir \
    black \
    pylint \
    pytest \
    ipython \
    jupyter \
    jupyterlab

# Switch to non-root user
USER vscode

# Install uv (fast Python package manager + version management)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
    echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> /home/vscode/.bashrc
ENV PATH="/home/vscode/.cargo/bin:$PATH"

# Install bun (for Python projects with JS/TS frontends)
RUN curl -fsSL https://bun.sh/install | bash && \
    echo 'export BUN_INSTALL="$HOME/.bun"' >> /home/vscode/.bashrc && \
    echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /home/vscode/.bashrc
ENV BUN_INSTALL="/home/vscode/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"

# Set Python to unbuffered mode (better for logs)
ENV PYTHONUNBUFFERED=1

🔍 Key additions:

  • Extended libraries: Support SQLite, compression (lzma), SSL, foreign function interface
  • Modern tools: ripgrep (rg) and fd-find for faster code/file search than grep/find
  • uv: Installed as non-root user, 10-100x faster than pip
  • bun: For hybrid Python/JavaScript projects (Django + React, Flask + Vue, etc.)
  • Jupyter/JupyterLab: For data science and interactive development

Install Dependencies

You can use either traditional pip OR modern uv. Both work with the same requirements.txt:

Using traditional pip:

pip install -r requirements.txt
pip list

Using uv (10-100x faster):

uv pip install -r requirements.txt
uv pip list

⚡ Why uv is faster:

  • Written in Rust (compiled language vs Python)
  • Parallel package downloads and installations
  • Better dependency resolution algorithm
  • Drop-in replacement for pip - same commands, faster execution

Verify Installation

Test all installed tools:

# Verify Python
python --version  # Should show Python 3.11.x

# Verify pip
pip --version

# Verify uv
uv --version

# Verify bun (for hybrid projects)
bun --version

# Verify modern tools
rg --version    # ripgrep (fast search)
fdfind --version # fd (fast find)
jq --version    # JSON processor

# Test your Flask/FastAPI app
python app.py
# or
uvicorn main:app --reload --host 0.0.0.0 --port 8000

🎉 Your advanced Python environment is ready for production development!

🔄 Changing Python Versions

Need to switch to Python 3.13, 3.14, or 3.15? Here are two methods:

Method 1: Change Docker Base Image (Simplest)

Edit the first line of your Dockerfile to use a different Python version:

# Python 3.11 (current LTS - default)
FROM python:3.11-slim

# Python 3.13 (latest stable)
FROM python:3.13-slim

# Python 3.14 (newer, if available)
FROM python:3.14-slim

After changing, rebuild the container: Cmd/Ctrl+Shift+P → "Dev Containers: Rebuild Container"

Method 2: Use uv for Version Management (Most Flexible)

With uv, you can install and switch between multiple Python versions WITHOUT rebuilding the container:

# List available Python versions
uv python list

# Install Python 3.13
uv python install 3.13

# Install Python 3.14
uv python install 3.14

# Install Python 3.15 (if available)
uv python install 3.15

# Create virtual environment with specific Python version
uv venv --python 3.13

# Install packages with specific Python version
uv pip install --python 3.13 -r requirements.txt

# Pin Python version for the project
uv python pin 3.13

✅ Benefits of uv for version management:

  • Switch Python versions instantly (no container rebuild)
  • Test code across multiple Python versions
  • Faster than pyenv or manual Python compilation
  • Integrated with uv's package management

📝 Version Selection Guide:

  • Python 3.11: Current LTS, most stable, best library compatibility
  • Python 3.13: Latest stable release, newest features, good compatibility
  • Python 3.14/3.15: Experimental/preview, testing only, not for production

⚡ Next.js Project Setup

Let's create a dev container for a Next.js application with all the modern tooling:

1

Create .devcontainer directory

In your Next.js project root, create the directory and files:

mkdir -p .devcontainer
cd .devcontainer
touch devcontainer.json Dockerfile
2

Create devcontainer.json

Add this configuration optimized for Next.js development:

{
  "name": "Next.js Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "typescript.preferences.importModuleSpecifier": "relative"
      }
    }
  },
  "runArgs": [
    "--cap-drop=ALL",
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW",
    "--cap-add=SETUID",
    "--cap-add=SETGID"
  ],
  "forwardPorts": [3000],
  "postCreateCommand": "npm install",
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-firewall.sh",
  "remoteUser": "node",
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
    "source=npm-cache-${devcontainerId},target=/home/node/.npm,type=volume"
  ]
}

💡 What's happening here:

  • Port 3000: Next.js default development server port
  • npm cache volume: Dramatically speeds up npm installs by persisting the cache
  • Tailwind CSS extension: IntelliSense for Tailwind classes in your JSX/TSX files
  • TypeScript settings: Optimized for Next.js import conventions
3

Create Dockerfile

Use Node.js 20 LTS for optimal Next.js performance:

FROM node:20

# Install Claude Code CLI globally
RUN npm install -g @anthropic-ai/claude-code

# Install common development tools and firewall dependencies
RUN apt-get update && apt-get install -y \
    git \
    curl \
    vim \
    iptables \
    ipset \
    dnsutils \
    aggregate \
    jq \
    sudo \
    && rm -rf /var/lib/apt/lists/*

# Create directory for Claude Code defaults
RUN mkdir -p /usr/local/share/claude-defaults

# Copy initialization scripts and templates
COPY init-firewall.sh /usr/local/bin/
COPY init-claude-config.sh /usr/local/bin/
COPY --chown=node:node settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=node:node mcp.json.template /usr/local/share/claude-defaults/mcp.json

# Set permissions and configure sudoers for both scripts
RUN chmod +x /usr/local/bin/init-firewall.sh && \
    chmod +x /usr/local/bin/init-claude-config.sh && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
    echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/node-claude-config && \
    chmod 0440 /etc/sudoers.d/node-firewall && \
    chmod 0440 /etc/sudoers.d/node-claude-config

# Set working directory
WORKDIR /workspace

# Keep container running
CMD ["sleep", "infinity"]

âš ī¸ Note:

Next.js 13+ requires Node.js 16.14 or later. We're using Node 20 LTS for the best performance and latest features.

4

Open in VS Code

Same as before - VS Code will detect the dev container configuration:

  1. Open the project folder in VS Code
  2. Click "Reopen in Container" when prompted (or use Command Palette → "Dev Containers: Reopen in Container")
  3. Wait for the container to build (first time takes a few minutes)
5

Test your setup

Once connected, verify everything is working:

# Check Node.js version
node --version  # Should show v20.x.x

# Check npm version
npm --version

# Check Claude Code is installed
claude --version

# Start Next.js development server
npm run dev

🎉 Your Next.js app should now be running at http://localhost:3000!

✅ Pro Tips:

  • The npm cache volume persists between container rebuilds, making installs much faster
  • Hot reload works perfectly - just save your files and see changes instantly
  • Use Claude Code to help scaffold new pages, components, and API routes

🤖 Multiple AI Assistants Setup

Want to use Claude Code, OpenAI Codex, AND Google Gemini in the same dev container? Here's how to set up all three AI assistants:

đŸŽ¯ Why Multiple AI Assistants?

  • Claude Code: Excellent for complex reasoning, refactoring, and architectural decisions
  • Codex: Great for code completion and quick code generation
  • Gemini: Strong at multimodal tasks and Google ecosystem integration
  • Use the right tool for each specific task!
1

Create devcontainer.json

Configure all three AI extensions with separate volume mounts:

{
  "name": "Multi-AI Dev Container",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "anthropic.claude-code",
        "openai.codex-vscode",
        "google.gemini-cli-vscode-ide-companion",
        "google.geminicodeassist",
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ],
      "settings": {
        "editor.formatOnSave": true
      }
    }
  },
  "runArgs": [
    "--cap-drop=ALL",
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW",
    "--cap-add=SETUID",
    "--cap-add=SETGID"
  ],
  "forwardPorts": [3000, 5000, 8000],
  "postCreateCommand": "npm install || pip install -r requirements.txt || echo 'No dependencies to install'",
  "postStartCommand": "sudo /usr/local/bin/init-claude-config.sh && sudo /usr/local/bin/init-codex-config.sh && sudo /usr/local/bin/init-firewall.sh",
  "remoteUser": "node",
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
    "source=codex-config-${devcontainerId},target=/home/node/.codex,type=volume",
    "source=gemini-config-${devcontainerId},target=/home/node/.gemini,type=volume"
  ]
}

💡 What's happening here:

  • Three separate volume mounts: Each AI gets its own persistent config directory
  • Multiple extensions: Install all AI assistant VS Code extensions at once
  • Multiple ports: Forward common ports for different project types
  • Flexible postCreateCommand: Works with npm, pip, or no dependencies
2

Create Dockerfile

Install all three CLI tools globally:

FROM node:20

ARG TZ
ENV TZ="$TZ"

ARG CLAUDE_CODE_VERSION=latest

# Upgrade npm to latest version (ensures security patches and latest features)
RUN npm install -g npm@latest

# Install development tools, compilers, and utilities with BuildKit cache mounts
# BuildKit cache mounts provide 5-10x faster rebuilds
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
  apt-get update && apt-get install -y --no-install-recommends \
  # Core utilities
  less curl wget ca-certificates gnupg2 software-properties-common \
  apt-transport-https lsb-release \
  # Version control
  git git-lfs gh \
  # Shell and terminal tools
  zsh fzf man-db \
  # Process and system tools
  procps htop tree sudo \
  # Network tools
  dnsutils net-tools iputils-ping telnet netcat-openbsd \
  iptables ipset iproute2 aggregate \
  # Archive and compression
  unzip zip bzip2 xz-utils \
  # Text processing and data
  jq yq sed gawk \
  # Fast search tools (modern replacements)
  ripgrep fd-find \
  # Build essentials (for native npm modules)
  build-essential gcc g++ make cmake pkg-config \
  # Python (for node-gyp and AI CLIs)
  python3 python3-pip python3-venv python3-dev \
  # Text editors
  nano vim tmux \
  # OpenSSL and crypto
  openssl libssl-dev \
  # Additional development libraries
  libz-dev libffi-dev libbz2-dev libreadline-dev libsqlite3-dev \
  libncurses5-dev libncursesw5-dev liblzma-dev \
  # Locales for proper encoding
  locales \
  # Browser automation (Chromium for Chrome DevTools MCP)
  chromium fonts-liberation libatk-bridge2.0-0 libatk1.0-0 \
  libatspi2.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 \
  libnspr4 libnss3 libxcomposite1 libxdamage1 libxfixes3 \
  libxkbcommon0 libxrandr2 xdg-utils \
  && apt-get clean && rm -rf /var/lib/apt/lists/*

# Set up locale (prevents encoding issues with AI CLIs)
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
  locale-gen
ENV LANG=en_US.UTF-8 \
  LANGUAGE=en_US:en \
  LC_ALL=en_US.UTF-8

# Ensure default node user has access to /usr/local/share
RUN mkdir -p /usr/local/share/npm-global && \
  chown -R node:node /usr/local/share

ARG USERNAME=node

# Persist bash history and configure PATH for npm global packages
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
  && mkdir /commandhistory \
  && touch /commandhistory/.bash_history \
  && chown -R $USERNAME /commandhistory \
  && echo 'export PATH=$PATH:/home/node/.npm-global/bin' >> /home/node/.bashrc \
  && chown node:node /home/node/.bashrc

# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true

# Create config directories and set permissions
RUN mkdir -p /home/node/.claude /home/node/.codex /home/node/.gemini /home/node/.npm-global/lib && \
  chown -R node:node /home/node/.claude /home/node/.codex /home/node/.gemini /home/node/.npm-global

# Default working directory
WORKDIR /workspaces

# Install git-delta for better diffs
ARG GIT_DELTA_VERSION=0.18.2
RUN ARCH=$(dpkg --print-architecture) && \
  wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
  dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
  rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"

# Set up non-root user
USER node

# Install global packages
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin

# Set the default shell to zsh rather than sh
ENV SHELL=/bin/zsh

# Set the default editor and visual
ENV EDITOR=nano
ENV VISUAL=nano

# Install zsh with powerline10k theme
ARG ZSH_IN_DOCKER_VERSION=1.2.0
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
  -p git \
  -p fzf \
  -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
  -a "source /usr/share/doc/fzf/examples/completion.zsh" \
  -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
  -a "export PATH=\$PATH:/home/node/.npm-global/bin" \
  -a "alias fd=fdfind" \
  -a "alias rg='rg --smart-case'" \
  -x

# Install all three AI CLIs with version pinning for Claude Code
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} && \
    npm install -g @openai/codex && \
    npm install -g @google/gemini-cli

# Install uv (fast Python package manager - 10-100x faster than pip)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
    echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> /home/node/.bashrc && \
    echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> /home/node/.zshrc
ENV PATH="/home/node/.cargo/bin:$PATH"

# Install bun (fast JavaScript runtime for hybrid Python/JS projects)
RUN curl -fsSL https://bun.sh/install | bash && \
    echo 'export BUN_INSTALL="$HOME/.bun"' >> /home/node/.bashrc && \
    echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /home/node/.bashrc && \
    echo 'export BUN_INSTALL="$HOME/.bun"' >> /home/node/.zshrc && \
    echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /home/node/.zshrc
ENV BUN_INSTALL="/home/node/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"

# Configure git to ignore Claude's local settings (Anthropic best practice)
RUN git config --global core.excludesfile ~/.gitignore_global && \
  echo ".claude/settings.local.json" > /home/node/.gitignore_global && \
  chown node:node /home/node/.gitignore_global

# Switch to root for final setup (scripts, sudoers, templates)
USER root

# Create directories for Claude Code and Codex defaults
RUN mkdir -p /usr/local/share/claude-defaults /usr/local/share/codex-defaults

# Copy all scripts and templates
COPY init-firewall.sh /usr/local/bin/
COPY init-claude-config.sh /usr/local/bin/
COPY init-codex-config.sh /usr/local/bin/
COPY --chown=node:node settings.json.template /usr/local/share/claude-defaults/settings.json
COPY --chown=node:node mcp.json.template /usr/local/share/claude-defaults/mcp.json
COPY --chown=node:node config.toml.template /usr/local/share/codex-defaults/config.toml

# Set permissions and configure sudoers for all init scripts
RUN chmod +x /usr/local/bin/init-firewall.sh && \
  chmod +x /usr/local/bin/init-claude-config.sh && \
  chmod +x /usr/local/bin/init-codex-config.sh && \
  echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
  echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-claude-config.sh" > /etc/sudoers.d/node-claude-config && \
  echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-codex-config.sh" > /etc/sudoers.d/node-codex-config && \
  chmod 0440 /etc/sudoers.d/node-firewall && \
  chmod 0440 /etc/sudoers.d/node-claude-config && \
  chmod 0440 /etc/sudoers.d/node-codex-config

# Switch back to node user for runtime
USER node

âš ī¸ Important Notes:

  • You'll need valid API keys for each service to authenticate
  • Each AI assistant authenticates independently
  • Build time will be longer with all three CLIs installed
  • Consider your API usage costs across multiple services
3

Authentication Setup

After the container builds, authenticate each AI service:

# Authenticate Claude Code
claude auth login
# You'll be prompted to enter your Anthropic API key

# Authenticate Codex
codex auth login
# You'll be prompted to enter your OpenAI API key

# Authenticate Gemini
gemini auth login
# You'll be prompted to enter your Google AI API key

🔑 Getting API Keys:

4

Verify Installation

Test that all three AI assistants are properly installed and configured:

# Check Claude Code
claude --version
claude auth status

# Check Codex
codex --version
codex auth status

# Check Gemini
gemini --version
gemini auth status

🎉 All three AI assistants should now be ready to use!

✅ Best Practices:

  • Task-specific usage: Use Claude for complex reasoning, Codex for quick completions, Gemini for multimodal work
  • API cost awareness: Monitor your usage across all three services to manage costs
  • Config persistence: Volume mounts ensure you don't need to re-authenticate when rebuilding containers
  • Security: Never commit API keys to your repository - keep them in the volume-mounted config directories
  • Extension conflicts: If you experience conflicts, you can disable individual extensions in VS Code settings

🔧 Troubleshooting

❌ Error: "Cannot connect to Docker daemon"

Cause: Docker Desktop isn't running.

Solution:

  1. Start Docker Desktop from your Applications folder
  2. Wait for the whale icon in your menu bar/system tray to show it's running
  3. Retry in VS Code: Cmd+Shift+P → "Dev Containers: Rebuild Container"

❌ Error: "Command not found" (npm, python, etc.)

Cause: Tool not installed in the container, or PATH not set correctly.

Solution:

  1. Check your Dockerfile has the correct base image (e.g., FROM node:20)
  2. Rebuild the container: Cmd+Shift+P → "Dev Containers: Rebuild Container"
  3. If still failing, check you're using the correct terminal (should say "Dev Container" in bottom-left)

âš ī¸ Container is very slow

Cause: Docker performance issues, especially on Mac/Windows.

Solutions:

  • Mac: Enable VirtioFS in Docker Desktop → Settings → Experimental Features
  • Windows: Ensure project is in WSL2 filesystem, not /mnt/c/
  • Use named volumes for node_modules: Add to mounts: "source=node-modules,target=/workspace/node_modules,type=volume"
  • Create .dockerignore file to exclude node_modules, .git from build context

â„šī¸ Port already in use

Cause: Another process is using the port (e.g., 3000, 5000).

Solutions:

  1. Find what's using the port: lsof -i :3000 (Mac/Linux) or netstat -ano | findstr :3000 (Windows)
  2. Kill the process or change your app's port
  3. Update forwardPorts in devcontainer.json to match new port

❌ Codex CLI: Landlock Sandbox Error

thread 'main' panicked at linux-sandbox/src/linux_run_main.rs:30:9:

error running landlock: Sandbox(LandlockRestrict)

Cause: Docker's LinuxKit kernel doesn't support Landlock (Linux security module). This is a known limitation of containerized environments.

Solution: Disable Codex sandboxing (safe in containers):

~/.codex/config.toml

sandbox_mode = "danger-full-access"
approval_policy = "never"

✅ Why This Is Safe:

  • Docker container provides process isolation
  • Network firewall restricts connections
  • Non-root user execution (node)
  • OpenAI's official recommendation for Docker

Verify it works:

codex --version
codex exec "echo 'Hello from Codex'"

📝 Note: This repository's devcontainer automatically applies this fix via init-codex-config.sh. Configuration persists across container rebuilds.

💡 Quick Debug Commands

# Check Docker is running
docker ps

# Check container logs
docker logs <container-name>

# View Docker disk usage
docker system df

# Clean up unused images/containers (frees space)
docker system prune -a

# Restart Docker Desktop
# Mac: Click whale icon → Quit → Relaunch
# Windows: Right-click whale icon → Quit → Relaunch

📚 Resources & Further Learning