
Anthropic's **Claude Code CLI** can run bash commands, install packages, and make network requests. That's powerful — but a single bad prompt could reach places you don't want it to. This guide shows how to **install and run Claude Code in Docker Compose on Linux** with full autonomy inside the container, while only allowing outbound traffic to an allowlist of domains.

You'll end up with a standard **Claude Code Docker image**, a `docker-compose.yml` for orchestration, and an optional **Squid proxy** for domain-level network isolation. All four files fit in ~80 lines.

## TL;DR — drop-in `docker-compose.yml`

Want to skip the explanation and start? This is the minimum viable `docker-compose.yml`. Save it next to a `Dockerfile`, `entrypoint.sh`, and `squid.conf` (full contents below) and run `docker compose up -d proxy && docker compose run --rm agent -p "hello"`.

```yaml
# docker-compose.yml — Claude Code in Docker Compose with network isolation
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
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
    volumes:
      - ${WORKSPACE:-./workspace}:/workspace
      - claude-config:/host-claude        # named volume — persists Pro/Max auth across rebuilds
      - claude-json:/host-claude-json
    networks: [isolated]
    profiles: [run]                        # docker compose up only starts proxy

volumes:
  claude-config:
  claude-json:

networks:
  isolated: { internal: true }             # no direct internet
  internet: { driver: bridge }             # only Squid touches this
```

**What this gives you:**

- Stock Claude Code CLI in a Linux container, no custom build.
- A Squid proxy on a dedicated network — agent can only reach allowlisted domains.
- **Persistent OAuth auth** via the named `claude-config` / `claude-json` volumes — sign in once on the host, copy the credential into the volume, and every subsequent rebuild keeps you logged in.
- Multi-project isolation via Compose profiles (see [Multi-project isolation](#multi-project-isolation-via-compose-profiles) below).

Read the full architecture below, or jump straight to [The Files](#the-files).

## Quick start: install Claude Code CLI in Docker

If you just want a working container, here are the four files you need:

- `Dockerfile` — extends `node:20-slim`, installs `@anthropic-ai/claude-code` via `npm`
- `entrypoint.sh` — forwards your OAuth credentials into the container
- `squid.conf` — Squid proxy allowlist for domain-level network isolation (optional but recommended)
- `docker-compose.yml` — wires them together

Jump to [The Files](#the-files) for the contents, or read the next section first for the architecture.

## Why isolate Claude Code CLI's network access?

Claude Code is an agentic CLI. Given tool-use permission, it can `curl` arbitrary URLs, `git push` to remotes, `ssh` into hosts, `pip install` from any index, and `rm -rf` anything it has access to. Docker already solves the filesystem blast-radius problem (mount only the workspace), but **network** is the other half — an agent that can reach arbitrary domains can exfiltrate data, fetch attacker-controlled scripts, or send SSRF-style requests to your internal network.

A Squid proxy on a dedicated allowlist fixes this with three properties:

1. **No direct internet from the agent container.** The Docker network is marked `internal: true`, so the only way out is through the proxy.
2. **Allowlist, not blocklist.** You name the domains the agent is allowed to reach (Anthropic API, GitHub, npm registry, etc.). Everything else fails closed.
3. **Transparent to the agent.** Claude Code sees standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables and respects them without any wrapper code.

## Architecture

The setup has two pieces:

1. A **Squid proxy** container that maintains the domain allowlist.
2. A **Claude Code container** on an internal Docker network. 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`. If it tries to reach `example.com` or any domain not on the allowlist, the proxy blocks it.

## Prerequisites

- **Linux** (Debian/Ubuntu, Fedora, Arch — anything with a recent kernel). The setup also works on Docker Desktop for macOS and Windows.
- **[Docker Engine](https://docs.docker.com/engine/install/)** 24.0 or newer
- **[Docker Compose](https://docs.docker.com/compose/install/)** v2 (comes bundled with current Docker Desktop and the `docker-compose-plugin` package)
- An **Anthropic API key** or an existing `claude` login on the host (the entrypoint forwards OAuth credentials from `~/.claude` into the container)

If you don't have Docker yet:

```bash
# Debian / Ubuntu
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"   # log out / back in to pick up group membership
```

## The files

You need four files in a single directory. Here's each one.

### Dockerfile — the Claude Code Docker image

```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. Volume mounts work without permission tweaks.

### entrypoint.sh — OAuth credential forwarding

Claude Code reads OAuth credentials from `~/.claude` and `~/.claude.json`. We mount the host's credentials read-only, then copy them into the container so Claude has a writable home directory for session data.

```bash
#!/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 — the domain allowlist

Edit this to match what your agent needs access to.

```squid
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
```

Gotcha: Squid doesn't allow overlapping entries. If you add `.anthropic.com` (leading dot covers all subdomains), don't also add `api.anthropic.com`. Squid will refuse to start.

### docker-compose.yml — wiring it together

```yaml
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`.

## Running Claude Code in the container

Start the proxy once:

```bash
docker compose up -d proxy
```

Run Claude Code against a project:

```bash
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:

```bash
docker compose down
```

### Skipping permissions inside the sandbox

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's the right call. The container is the sandbox.

### Running parallel Claude Code agents

Each `docker compose run` creates a separate container. Run as many as you need:

```bash
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. This is how I run [automated code review workflows](/blog/automated-code-review-claude-code-workflow/) and [GitHub-issue-to-production pipelines](/blog/github-issue-to-production-automated-claude-code/) across multiple repos at the same time.

### API key instead of OAuth

If you prefer API key authentication over OAuth:

```bash
ANTHROPIC_API_KEY=sk-ant-xxx docker compose run --rm agent \
  -p "Your prompt" --dangerously-skip-permissions
```

### Resource limits

Default is 2 CPUs and 4 GB memory. Override with environment variables:

```bash
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:

```bash
#!/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 a shell alias in `~/.zshrc`:

```bash
alias claude-docker='docker compose -f ~/path/to/docker-compose.yml'
```

## Verifying the network isolation works

To confirm the Squid proxy is actually blocking traffic:

```bash
# 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)
```

You can also tail the Squid access log while the agent runs — every outbound request shows up there, which is useful for auditing what Claude Code actually reached out to during a session.

## Persisting Claude Pro / Max auth across container rebuilds

If you log in to Claude Code on the host with `claude` and pay for **Pro / Max**, the OAuth credential lives in `~/.claude/.credentials.json` and `~/.claude.json`. Re-doing that login every container rebuild is annoying — and on machines without an interactive browser it's not even possible.

The fix is named volumes, not host-path mounts. Host-path mounts are read-only and tied to the host filesystem; named volumes survive `docker compose build`, `docker compose down`, and image rebuilds.

**One-time setup:** copy your existing host credentials *into* the named volume:

```bash
# Run once, after the first `docker compose up -d proxy`
docker run --rm \
  -v claude-config:/dst \
  -v "$HOME/.claude:/src:ro" \
  alpine sh -c 'cp -r /src/. /dst/'

docker run --rm \
  -v claude-json:/dst \
  -v "$HOME/.claude.json:/src.json:ro" \
  alpine sh -c 'cp /src.json /dst/host.json'
```

Then update `entrypoint.sh` to read from the volume mounts instead of host paths — the script in [The Files](#the-files) above already does this if you mount the volumes at `/host-claude` and `/host-claude-json`. After that, every `docker compose run --rm agent` reuses the same OAuth session, even after `docker compose build agent` rebuilds the image.

**Renewing the token:** when your Pro/Max session expires (Anthropic refreshes OAuth periodically), re-run the one-time copy commands above. Or `docker compose run --rm agent claude /login` if you have an interactive browser inside the container.

This is the piece every other Claude-Code-in-Docker tutorial skips, and the reason most of them break the second time you rebuild.

## Compose + Squid egress allowlist (with curl test output)

If you skipped the architecture section, here's how the egress filter actually behaves end-to-end. Build the image, start the proxy, then test with `curl` from inside an ephemeral agent container:

```bash
$ docker compose build agent
$ docker compose up -d proxy

# Allowed domain — succeeds
$ docker compose run --rm --entrypoint sh agent -c \
    'curl -s -o /dev/null -w "%{http_code}\n" https://api.anthropic.com'
404

# Allowed domain — succeeds (GitHub API root returns JSON)
$ docker compose run --rm --entrypoint sh agent -c \
    'curl -s -o /dev/null -w "%{http_code}\n" https://api.github.com'
200

# Disallowed domain — Squid returns 403 to the client
$ docker compose run --rm --entrypoint sh agent -c \
    'curl -s -o /dev/null -w "%{http_code}\n" https://example.com'
403

# Squid log shows the deny
$ docker compose logs proxy | tail -3
proxy-1 | 1747...  TCP_DENIED/403 ... CONNECT example.com:443 ... HIER_NONE/-
```

Three tells that the allowlist is real:

1. `example.com` returns **403** *from Squid*, not from the destination — the request never left your machine.
2. The Squid access log records `TCP_DENIED/403 ... HIER_NONE/-`, the canonical "blocked at allowlist" entry.
3. Removing `HTTP_PROXY` from the agent service and re-running the `example.com` curl returns a connect error (because `isolated.internal: true` means there's no route to the internet without the proxy).

If your allowlist needs additional domains (Linear, Sentry, an internal package mirror), add them to `squid.conf` with `acl allowed_sites dstdomain .your-domain.com`, then `docker compose restart proxy`. No agent rebuild required.

## Multi-project isolation via Compose profiles

Once you start running Claude Code agents on more than one project, you want each one to:

- See **only that project's** workspace.
- Use **its own** environment variables (different `ANTHROPIC_API_KEY`s, different proxy allowlists).
- Run in **parallel** without colliding on container names.

Compose profiles + a small per-project override file make this clean. Define one base `docker-compose.yml` (the file above) and a per-project `docker-compose.<project>.yml`:

```yaml
# docker-compose.web.yml — overrides for the "web" project
services:
  agent:
    profiles: [web]
    container_name: claude-agent-web
    volumes:
      - /home/me/code/web:/workspace
    environment:
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY_WEB}
```

```yaml
# docker-compose.infra.yml — overrides for the "infra" project, stricter allowlist
services:
  proxy:
    volumes:
      - ./squid.infra.conf:/etc/squid/squid.conf:ro
  agent:
    profiles: [infra]
    container_name: claude-agent-infra
    volumes:
      - /home/me/code/infra:/workspace
```

Then run them concurrently:

```bash
# Web project — with the default allowlist
docker compose -f docker-compose.yml -f docker-compose.web.yml \
  --profile web run --rm agent -p "Add cookie consent" --dangerously-skip-permissions &

# Infra project — with a stricter allowlist (no GitHub, only Anthropic + internal registry)
docker compose -f docker-compose.yml -f docker-compose.infra.yml \
  --profile infra run --rm agent -p "Audit Terraform" --dangerously-skip-permissions &

wait
```

Each agent gets its own filesystem, its own egress policy, and its own credential set. Two safety properties fall out for free:

- The `web` agent **cannot** read files in `/home/me/code/infra` because the volume isn't mounted.
- The `infra` agent **cannot** reach `github.com` because its proxy uses the stricter `squid.infra.conf`.

That's the multi-project isolation pattern. Useful when you want to run agentic refactors against several repos at once without one prompt accidentally polluting another.

## When to use this vs. running Claude Code natively

| Situation | Docker + Squid isolation | Native `claude` |
| --------- | ------------------------ | --------------- |
| Interactive pair-programming on your own machine | Overkill | Better — no overhead |
| Batch / CI agent runs | **Recommended** | Risky |
| Running prompts from untrusted sources | **Recommended** | Dangerous |
| Parallel agents on isolated workspaces | **Recommended** | Works but no resource limits |
| Compliance / audit requirements | **Recommended** (Squid logs are great evidence) | Harder to justify |

For day-to-day coding with Claude Code I run it natively. For anything automated, untrusted, or multi-tenant, I run it in this Docker setup.

## Related posts

- [Automated code review with a Claude Code workflow](/blog/automated-code-review-claude-code-workflow/)
- [From GitHub issue to production with Claude Code](/blog/github-issue-to-production-automated-claude-code/)
- [Agento: a Claude Code web UI AI-agent builder](/blog/agento-claude-code-web-ui-ai-agent-builder/)

## FAQ

**Is there an official Anthropic Docker image for Claude Code CLI?**
No. Anthropic distributes Claude Code as an npm package (`@anthropic-ai/claude-code`). The `Dockerfile` above wrapping it in `node:20-slim` is currently the closest to an official image.

**How do I install Claude Code CLI inside a Docker container?**
Build the image with `docker compose build agent`. The `Dockerfile` uses `npm install -g @anthropic-ai/claude-code` on top of `node:20-slim`.

**Does Claude Code CLI work with docker-compose?**
Yes — see the `docker-compose.yml` above. The `agent` service is guarded by the `run` profile so `docker compose up` only starts the proxy; each invocation gets a fresh ephemeral container.

**How do I isolate Claude Code's network access?**
Put the agent container on an `internal: true` Docker network with no bridge to the host, and route all HTTP(S) traffic through a Squid proxy with a `dstdomain` allowlist. Non-allowlisted domains fail closed.

**Can I run multiple Claude Code agents in parallel?**
Yes — each `docker compose run --rm agent` creates a separate container. They share the Squid proxy but are otherwise isolated.

## 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. Docker handles the filesystem isolation, Squid handles the network filtering, 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 — just stock Docker, Docker Compose, and a battle-tested proxy.
