Running Claude Code CLI in Docker with Network Isolation
How to run Claude Code CLI inside a Docker container with a Squid proxy that only allows traffic to specific domains. Full setup with docker-compose, OAuth credential forwarding, and parallel agent support.
On this page ▾
Claude Code CLI can run bash commands, install packages, and make network requests. That’s powerful, but it also means a single bad prompt could reach places you don’t want it to. I wanted to run Claude Code in an environment where it has full autonomy over the filesystem but can only talk to a handful of approved domains.
Here’s how I set it up with Docker and a Squid proxy.
What We’re Building
The setup has two pieces:
- A Squid proxy that maintains a domain allowlist. Only traffic to approved domains gets through.
- A Claude Code container that sits on an internal Docker network with no direct internet access. All its traffic goes through the proxy.
┌─────────────────────────────────────────────┐
│ Host │
│ │
│ ┌────────────┐ ┌─────────────────────┐ │
│ │ Squid │◄───│ isolated network │ │
│ │ (allowlist)│ │ ┌───────────────┐ │ │
│ └─────┬──────┘ │ │ Claude Agent │ │ │
│ │ │ │ (ephemeral) │ │ │
│ internet │ └───────────────┘ │ │
│ network └─────────────────────┘ │
│ │ │
│ ▼ │
│ Internet (filtered) │
└─────────────────────────────────────────────┘
The agent can do whatever it wants inside /workspace. But if it tries to reach example.com or any domain not on the list, the proxy blocks it.
The Files
You need four files. Here’s each one.
Dockerfile
FROM node:20-slim
RUN npm install -g @anthropic-ai/claude-code
RUN apt-get update && apt-get install -y git curl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
USER node
WORKDIR /workspace
ENTRYPOINT ["entrypoint.sh"]
The node user in node:20-slim has UID 1000, which matches most Linux host users. This makes volume mount permissions work without extra configuration.
entrypoint.sh
Claude Code reads OAuth credentials from ~/.claude and ~/.claude.json. We mount the host’s credentials as read-only, then copy them into the container so Claude has a writable home directory for session data.
#!/bin/bash
set -e
if [ -d "/host-claude" ]; then
mkdir -p "$HOME/.claude"
if [ -f "/host-claude/.credentials.json" ]; then
cp "/host-claude/.credentials.json" "$HOME/.claude/.credentials.json"
fi
for f in statsig.json settings.json; do
if [ -f "/host-claude/$f" ]; then
cp "/host-claude/$f" "$HOME/.claude/$f"
fi
done
fi
if [ -f "/host-claude.json" ]; then
cp "/host-claude.json" "$HOME/.claude.json"
fi
exec claude "$@"
squid.conf
This is the allowlist. Edit it to match what your agent needs access to.
acl allowed_sites dstdomain .anthropic.com
acl allowed_sites dstdomain .github.com
acl allowed_sites dstdomain .googleapis.com
acl allowed_sites dstdomain registry.npmjs.org
acl allowed_sites dstdomain .sentry.io
acl allowed_sites dstdomain .statsigapi.net
acl SSL_ports port 443
acl CONNECT method CONNECT
http_access allow CONNECT allowed_sites
http_access allow allowed_sites
http_access deny all
http_port 3128
One thing to watch out for: Squid doesn’t allow overlapping entries. If you add .anthropic.com (with the leading dot, which covers all subdomains), don’t also add api.anthropic.com. Squid will refuse to start.
docker-compose.yml
services:
proxy:
image: ubuntu/squid
volumes:
- ./squid.conf:/etc/squid/squid.conf:ro
networks:
- isolated
- internet
restart: unless-stopped
agent:
build: .
depends_on:
- proxy
environment:
- HTTP_PROXY=http://proxy:3128
- HTTPS_PROXY=http://proxy:3128
- http_proxy=http://proxy:3128
- https_proxy=http://proxy:3128
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
volumes:
- ${WORKSPACE:-./workspace}:/workspace
- ${HOME}/.claude:/host-claude:ro
- ${HOME}/.claude.json:/host-claude.json:ro
networks:
- isolated
deploy:
resources:
limits:
cpus: "${AGENT_CPUS:-2}"
memory: "${AGENT_MEMORY:-4g}"
profiles:
- run
networks:
isolated:
internal: true
internet:
driver: bridge
The agent service is under the run profile, so docker compose up only starts the proxy. Agents are started on demand with docker compose run.
Usage
Start the proxy once:
docker compose up -d proxy
Run Claude Code against a project:
WORKSPACE=/path/to/project docker compose run --rm agent \
-p "Your prompt here" --dangerously-skip-permissions
Everything after agent is passed directly to the Claude CLI. You get access to every flag Claude supports without any wrapper getting in the way.
When you’re done:
docker compose down
Skipping Permissions
The --dangerously-skip-permissions flag tells Claude Code to execute all tool calls without asking for confirmation. Normally you wouldn’t want this, but inside an isolated container with filtered network access, it makes sense. The container is the sandbox.
Parallel Agents
Each docker compose run creates a separate container. Run as many as you need:
WORKSPACE=./project-a docker compose run --rm agent -p "Fix tests" --dangerously-skip-permissions &
WORKSPACE=./project-b docker compose run --rm agent -p "Add logging" --dangerously-skip-permissions &
wait
They share the proxy but are otherwise completely isolated from each other.
API Key Instead of OAuth
If you prefer API key authentication over OAuth:
ANTHROPIC_API_KEY=sk-ant-xxx docker compose run --rm agent \
-p "Your prompt" --dangerously-skip-permissions
Resource Limits
Default is 2 CPUs and 4GB memory. Override with environment variables:
AGENT_CPUS=4 AGENT_MEMORY=8g WORKSPACE=./project docker compose run --rm agent \
-p "Heavy task" --dangerously-skip-permissions
Optional: Wrapper Script
If you don’t want to type docker compose -f /path/to/docker-compose.yml every time, a small wrapper script helps:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export WORKSPACE="${WORKSPACE:-$(pwd)}"
case "${1:-help}" in
up) docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d proxy ;;
down) docker compose -f "$SCRIPT_DIR/docker-compose.yml" down ;;
status) docker compose -f "$SCRIPT_DIR/docker-compose.yml" ps ;;
run) shift; docker compose -f "$SCRIPT_DIR/docker-compose.yml" run --rm agent "$@" ;;
*) echo "Usage: $0 {up|down|status|run [claude args...]}" ;;
esac
Or add a shell alias to your ~/.zshrc:
alias claude-docker='docker compose -f ~/path/to/docker-compose.yml'
Then from anywhere:
claude-docker run --rm agent -p "Your prompt" --dangerously-skip-permissions
Verifying the Isolation
To confirm the proxy is actually blocking traffic:
# This should fail (not on the allowlist)
docker compose run --rm agent curl -s https://example.com
# Connection refused
# This should succeed (on the allowlist)
docker compose run --rm agent curl -s -o /dev/null -w "%{http_code}" https://api.anthropic.com
# 404 (reachable, just no content at the root)
Wrapping Up
This setup gives you the best of both worlds. Claude Code gets full autonomy to read files, write code, run tests, and install packages. But it can only talk to the domains you approve. The Squid proxy handles the filtering, Docker handles the isolation, and docker compose run handles spinning up ephemeral agents.
The whole thing is about 80 lines of configuration across four files. No custom tooling, no complex orchestration.