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
# 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
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
docker compose up -d --build
docker compose exec claude-code bash
# Inside the container:
claude
/login