Running Claude Code in Docker: Anthropic's Official Devcontainer, No VS Code Required
How to use Anthropic's official Claude Code devcontainer from the terminal — per-project isolation, egress firewall, port forwarding, no VS Code required.
The previous post covered Docker Sandboxes — the microVM-based sandbox that ships with Claude Code. Strong isolation, no port forwarding. For a Next.js dev server you need a regular Docker container.
The obvious answer is a custom Dockerfile: node:20-bookworm, install Claude Code, switch to a non-root user, mount your project. It works. But Anthropic ships an official devcontainer for Claude Code in their claude-code repo. It’s what the VS Code devcontainer integration uses, and it has things a DIY Dockerfile won’t: a default-deny egress firewall, non-root execution with properly restricted sudo, and a Dockerfile Anthropic maintains.
The problem is it’s designed for VS Code. claude-devcontainer wires it up for the terminal. Per-project isolated containers, dev server port forwarding, auth in Docker volumes (never on your host filesystem), and automatic cleanup when you exit Claude.
Prerequisites
- Docker Desktop — installed and running
- Node.js v18+ — for the devcontainer CLI
- Git
- python3
Install
1
2
3
4
5
6
7
8
9
10
11
12
# 1. Install the devcontainer CLI
npm install -g @devcontainers/cli
# 2. Clone and run setup
git clone https://github.com/mmasters/claude-container.git
cd claude-container
chmod +x setup.sh
./setup.sh
# 3. Source the shell functions
echo 'source ~/.claude-devcontainer/claude-dev.sh' >> ~/.zshrc
source ~/.zshrc
setup.sh sparse-clones Anthropic’s .devcontainer directory from anthropics/claude-code, patches the firewall script to allow inbound on ports 3000–9000, and installs the shell functions to ~/.claude-devcontainer/. The first claude-dev invocation builds the Docker image (2–3 minutes). Subsequent starts are fast.
Usage
1
2
3
claude-dev ~/projects/my-app # Start in a project, port 3000
claude-dev ~/projects/my-api -p 3001 # Custom port
claude-dev # Current directory
When you exit Claude (via /exit or Ctrl+C), the container stops automatically and frees the port.
How It Works
Each project gets its own instance under ~/.claude-devcontainer/instances/<project-name>/. The first claude-dev call creates a self-contained copy of the build files — Dockerfile, firewall script, and a generated devcontainer.json with the workspace mount, port, and your git config baked in.
1
2
3
4
5
6
7
8
9
~/.claude-devcontainer/
├── claude-dev.sh
├── template/.devcontainer/ ← Anthropic's devcontainer (patched)
│ ├── devcontainer.json
│ ├── Dockerfile
│ └── init-firewall.sh
└── instances/
├── my-app/.devcontainer/ ← per-project, fully self-contained
└── my-api/.devcontainer/
No symlinks. Each instance gets full copies of the build files so template changes don’t break running instances. Your project directory is bind-mounted into /workspace at runtime — not baked into the image.
The Firewall
The stock Anthropic devcontainer has a default-deny egress firewall. Outbound traffic is whitelisted to npm, GitHub, and the Anthropic API. Nothing else goes out.
setup.sh patches init-firewall.sh with one additional rule:
1
iptables -A INPUT -p tcp --dport 3000:9000 -j ACCEPT
Inbound connections on 3000–9000 are open so dev servers are reachable from the host browser. Egress stays locked down. Claude can’t phone home to arbitrary servers no matter what it tries.
To allow additional outbound access (PyPI, a private registry, etc.), add domains to the allowlist and rebuild:
1
2
nano ~/.claude-devcontainer/template/.devcontainer/init-firewall.sh
claude-dev-rebuild
Port Forwarding
The firewall handles the Docker side, but your dev server needs to bind to 0.0.0.0 instead of 127.0.0.1. Inside a container, 127.0.0.1 is the container’s own loopback — the host port forward can’t reach it.
For Next.js, add the flag in package.json:
1
2
3
4
5
{
"scripts": {
"dev": "next dev --hostname 0.0.0.0"
}
}
Or just run it directly: npx next dev --hostname 0.0.0.0. The same applies to any dev server — Vite, Express, Flask, whatever. If the server starts but localhost:3000 doesn’t respond from the host, this is always the fix.
Screenshots
The old DIY setup couldn’t handle screenshots — macOS puts clipboard paths like /var/folders/.../Screenshot.png in the clipboard, and that path doesn’t exist inside a Linux container.
This setup mounts ~/Desktop/Screenshots read-only at the same host path inside the container. Drag a screenshot into iTerm2 and the path resolves correctly. No manual file moves.
If macOS saves screenshots to a different directory (check System Settings → Screenshots), update the screenshots_mount line in ~/.claude-devcontainer/claude-dev.sh and run claude-dev-rebuild.
Authentication
The first claude-dev run will prompt you to authenticate. Auth is stored in a Docker volume managed by the devcontainer runtime — not mounted from ~/.claude on your host. The credentials can’t touch your host filesystem, and they persist across runs for that instance.
Managing Instances
1
2
3
4
5
6
7
claude-dev-ls # List all instances with port and status
claude-dev-down # Stop all running instances
claude-dev-down my-app # Stop a specific instance
claude-dev-rm my-app # Stop + delete instance config
claude-dev-shell ~/projects/my-app # Shell into a container (no Claude)
claude-dev-rebuild # Rebuild from updated template
claude-dev-nuke # Kill everything, clear volumes, start over
claude-dev-shell is useful for debugging — same container, same mounts, drops you into zsh instead of launching Claude.
claude-dev-nuke is the emergency reset: kills all devcontainer containers, clears all instances, and prunes dangling volumes. Use it if the firewall gets into a bad state or DNS stops working inside the container.
The Bottom Line
The DIY Dockerfile approach works, but the egress firewall is the piece it’s missing. If Claude decides to exfiltrate something, a plain Docker container will let it call any host on the internet. The devcontainer’s default-deny egress means the only outbound destinations are npm, GitHub, and Anthropic’s API.
claude-devcontainer wraps Anthropic’s official setup for terminal use. Install once, source the functions, and claude-dev drops you into a properly isolated Claude Code session in any project directory.