# Running Claude Code in Docker

The Dockerfile and docker-compose.yml I use to run Claude Code in a container.

Two reasons for running it this way instead of on the host directly. First, security: whatever Claude Code touches stays boxed inside the container, not loose on my actual machine. Second, it's what lets me actually use `--dangerously-skip-permissions`: that flag skips every one of Claude Code's permission prompts, which I'm not comfortable granting on the host, but is fine once the whole thing is sandboxed in a container with nothing else to reach.

## Dockerfile

```dockerfile
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0

# --- Node.js (required to run Claude Code) + git + basic tooling ---
ARG NODE_MAJOR=22
RUN apt-get update && apt-get install -y --no-install-recommends \
        curl \
        git \
        ca-certificates \
        gnupg \
        unzip \
    && curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/*

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

# --- Non-root user ---
# Claude Code refuses to start with --dangerously-skip-permissions when
# running as root/sudo, so a dedicated non-root user is required.
ARG USERNAME=claude
ARG USER_UID=1000
ARG USER_GID=$USER_UID

RUN if getent group $USER_GID > /dev/null; then \
        EXISTING_GROUP=$(getent group $USER_GID | cut -d: -f1); \
        groupmod -n $USERNAME $EXISTING_GROUP; \
    else \
        groupadd --gid $USER_GID $USERNAME; \
    fi \
    && if getent passwd $USER_UID > /dev/null; then \
        EXISTING_USER=$(getent passwd $USER_UID | cut -d: -f1); \
        usermod -l $USERNAME -g $USER_GID -d /home/$USERNAME -m -s /bin/bash $EXISTING_USER; \
    else \
        useradd --uid $USER_UID --gid $USER_GID -m -s /bin/bash $USERNAME; \
    fi

# Workspace mount point for your project
RUN mkdir -p /workspace && chown -R $USERNAME:$USERNAME /workspace

# Keep Claude Code's auth/config in the user's home so it can be persisted
# via a named volume across container rebuilds (see docker-compose.yml below)
ENV CLAUDE_CONFIG_DIR=/home/$USERNAME/.claude
RUN mkdir -p $CLAUDE_CONFIG_DIR && chown -R $USERNAME:$USERNAME /home/$USERNAME

# Avoid dotnet/git "unsafe repository" complaints on the bind-mounted folder
USER $USERNAME

# Local git identity for commits. No remote credentials (SSH key / PAT) are
# configured anywhere in this image, so `git push` / `git pull` against a
# private remote will fail at auth: only local commits are possible.
ARG GIT_USER_NAME="Claude Agent"
ARG GIT_USER_EMAIL="claude-agent@local"

RUN git config --global --add safe.directory /workspace \
    && git config --global user.name "$GIT_USER_NAME" \
    && git config --global user.email "$GIT_USER_EMAIL" \
    && dotnet nuget locals all --clear

WORKDIR /workspace

CMD ["bash"]
```

`.NET SDK` is the base image because the project I run this against is a .NET solution. Swap it for whatever base fits your own project, the Node and Claude Code CLI steps stay the same.

## docker-compose.yml

```yaml
services:
  claude-code:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        GIT_USER_NAME: "Claude Agent"
        GIT_USER_EMAIL: "claude-agent@local"
    image: claude-code
    container_name: claude-code
    stdin_open: true   # keep STDIN open (needed for `docker compose run`/attach)
    tty: true           # allocate a pseudo-TTY so the Claude Code UI renders correctly
    volumes:
    - type: bind
      source: /path/to/your/project
      target: /workspace

    - type: volume
      source: claude-code-config
      target: /home/claude/.claude
    working_dir: /workspace
    command: bash

volumes:
  claude-code-config:
```

## Running it

```bash
docker compose up -d --build
docker compose exec claude-code bash

# Inside the container:
claude
/login
```

---

*Co-authored with Claude.*
