I use Claude Code for everything. Writing code, debugging shit, deploying infrastructure, managing repos, writing articles for this very blog, and even automating browser sessions on the fly — browsing the web, filling out forms, scraping data from sites that hide behind Cloudflare. It’s become the backbone of how I work. But here’s the thing about giving an AI agent full access to your system: it’s fucking terrifying. Not because Claude is malicious, but because it runs with --dangerously-skip-permissions and has the power to do whatever it wants. One bad command and your host system is toast.
The obvious answer is: put it in a container. But actually doing that properly is a different story. You can’t just docker run node && npm i -g @anthropic-ai/claude-code and call it a day. Claude Code needs a real development environment — compilers, linters, formatters, package managers, database clients, infrastructure tools. It needs git with SSH keys. It needs Docker itself for when it builds and runs containers. It needs to understand where it is and what tools are available to it.
So I built docker-claude-code. A fully loaded Docker image that gives Claude Code everything it needs to be a productive developer — isolated from your host, with proper tooling, proper permissions, and a startup script that handles all the annoying setup automatically.
Why Not Just Run It Natively?
Because installing development tools globally on your host machine is for people who enjoy debugging PATH conflicts on a Sunday afternoon.
Here’s what happens when you run Claude Code natively:
- It has access to your entire filesystem. Every SSH key, every
.envfile, every credential stored anywhere - When it installs packages or runs build commands, those affect your global system state
- If it decides to
rm -rfsomething it shouldn’t, that’s your actual system getting nuked - Different projects need different tool versions, and your host becomes a graveyard of conflicting installations
- You can’t easily reproduce your Claude environment on another machine
Running it in a Docker container solves all of this. Claude gets root-level access inside the container, can install whatever the fuck it wants, break whatever it wants, and your host doesn’t care. Blow up the container, spin up a new one, you’re back in 10 seconds.
What’s In The Box
This isn’t some minimal Alpine image with Python slapped on top. It’s a full development workstation packed into a container. Ubuntu 22.04 base with everything a developer — human or AI — actually needs:
Languages and Runtimes
- Go 1.25.5 with the full toolchain — golangci-lint, gopls, delve, staticcheck, gofumpt, gotests, gomodifytags, impl. Not just the compiler, the entire Go development experience
- Python 3.12 via pyenv with flake8, black, isort, autoflake, pyright, mypy, vulture, pytest, poetry, pipenv. Every linter and formatter you’d actually use
- Node.js LTS with eslint, prettier, typescript, ts-node, yarn, pnpm, nodemon, pm2, framework CLIs for React, Vue, and Angular, plus newman, lighthouse, and storybook
- C/C++ — gcc, g++, make, cmake, clang-format, valgrind, gdb, strace, ltrace
Infrastructure and DevOps
- Docker CE with Docker Compose — yes, Docker inside Docker. The host’s Docker socket is mounted so Claude can build images, run containers, manage compose stacks. Docker-in-Docker madness, fully enabled
- Terraform for infrastructure as code
- kubectl + helm for Kubernetes management
- GitHub CLI (gh) for repo, PR, and issue management
Databases and Utilities
- Database clients for SQLite, PostgreSQL, MySQL, and Redis
- jq, ripgrep, fd-find, bat, exa, silversearcher-ag — the modern CLI toolkit
- shellcheck and shfmt for shell script quality
- httpie, curl, wget for HTTP work
The key detail: the container auto-generates a CLAUDE.md file in the workspace that lists every single tool available. Claude reads this on startup and immediately knows what it can use. No fumbling around trying to figure out what’s installed.
How It Actually Works
The magic is in the entrypoint script. When the container starts, it does a bunch of shit that would be painful to do manually every time:
UID/GID matching. The entrypoint detects the owner of the mounted workspace directory and adjusts the container’s claude user to match. Files created inside the container have the correct ownership on the host. No more chown -R bullshit after every session.
Docker socket permissions. If you mount /var/run/docker.sock, the entrypoint matches the docker group GID inside the container to the socket’s GID on the host. Docker commands just work without permission errors.
Git identity. Pass CLAUDE_GIT_NAME and CLAUDE_GIT_EMAIL environment variables and git is configured automatically. Commits from inside the container have your identity.
Session continuity. The entrypoint runs Claude with --continue first, which resumes the last conversation from the current working directory. Each directory gets its own conversation history, so --continue in ~/project-a picks up where you left off in that project, not some other one. If there’s no previous session for that directory, it falls back to starting fresh. Stop a container, come back hours later, start it again — Claude picks up exactly where it left off in that specific project.
Auto-update. Claude Code updates itself on every container start. The image ships with a specific version, but the native installer can self-update so you always get the latest without rebuilding the image.
Setup
One-liner install that handles everything:
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-claude-code/master/install.sh | bashThis creates the SSH keys, pulls the image, and drops a wrapper script at /usr/local/bin/claude. After that, you just run:
claudeThat’s it. The wrapper script handles container lifecycle automatically — creates a new container if one doesn’t exist for the current directory, restarts and reattaches if one already exists. Each workspace gets its own container named after the directory path.
If you want manual control, set your git identity in your shell profile:
export CLAUDE_GIT_NAME="Your Name"
export CLAUDE_GIT_EMAIL="[email protected]"And the wrapper script runs the container with all the right mounts:
docker run -it \
--network host \
-e CLAUDE_GIT_NAME="$CLAUDE_GIT_NAME" \
-e CLAUDE_GIT_EMAIL="$CLAUDE_GIT_EMAIL" \
-e CLAUDE_WORKSPACE="$PWD" \
-v "$HOME/.ssh/claude-code:/home/claude/.ssh" \
-v "$HOME/.claude:/home/claude/.claude" \
-v "$PWD:$PWD" \
-v /var/run/docker.sock:/var/run/docker.sock \
--name "$container_name" \
psyb0t/claude-code:latestNotice the path mounting: $PWD:$PWD. The workspace is mounted at its exact host path inside the container. This matters because when Claude runs Docker commands inside the container — mounting volumes, referencing paths in compose files — the paths match the host. No path translation headaches.
The Workspace-Per-Container Model
The wrapper script creates one container per workspace directory. cd ~/project-a && claude gives you one container. cd ~/project-b && claude gives you another. Each has its own Claude conversation history, its own installed dependencies, its own state.
This means Claude can install project-specific tools, modify the environment, break things spectacularly — and it only affects that one project’s container. Your other projects are untouched. Nuke a container, spin up a fresh one, zero collateral damage.
The ~/.claude directory is mounted as a volume, so your Claude configuration, API keys, and settings persist across all containers. SSH keys are similarly shared. The isolation is at the workspace level, not the identity level.
Docker-in-Docker
This is where it gets interesting. The host’s Docker socket is mounted into the container, which means Claude can:
- Build Docker images from Dockerfiles in your project
- Run
docker-compose upto spin up multi-service stacks - Pull and run any image from Docker Hub
- Manage containers, networks, volumes — the full Docker API
Since the workspace is mounted at its real host path, volume mounts from inside Claude’s container resolve correctly on the host. Claude can write a docker-compose.yml, run it, and everything just works because the paths are consistent between the container and the host.
Security Model
Let’s be real — this thing runs with --dangerously-skip-permissions. That flag is in the name for a reason. Inside the container, Claude has passwordless sudo and can do whatever it wants.
The security boundary is the container itself. Claude can’t touch your host filesystem beyond the mounted workspace and config directories. It can’t access other projects. It can’t modify your host’s system files or installed packages. If it goes rogue, docker stop and docker rm and it’s gone.
The Docker socket mount is the one exception — since it has access to the host’s Docker daemon, it can theoretically interact with other containers on the host. If that concerns you, don’t mount the socket. You’ll lose Docker-in-Docker capability but gain full network isolation.
SSH keys are mounted read-only from a dedicated ~/.ssh/claude-code directory. Generate a separate keypair for Claude, add only the necessary deploy keys to your repos, and keep your personal SSH keys out of the container entirely.
The Bottom Line
I run every Claude Code session inside this container now. The isolation means I don’t think twice about letting it install packages, modify configs, or run whatever commands it needs. The tooling means it rarely needs to install anything because everything’s already there. The session continuity means I can close my terminal, come back hours later, and pick up exactly where I left off.
It’s a fully loaded development workstation that exists only when you need it and disappears when you don’t. No cleanup, no conflicts, no “what the fuck did Claude install on my system” moments.
Go grab it: github.com/psyb0t/docker-claude-code
Licensed under WTFPL — because the only thing more dangerous than an AI with root access is an AI with root access and a restrictive license.