Run Claude Code inside Docker on the Arch host with a reusable image and per-project mounts. The container is created fresh for each run, can read and edit only the mounted project directory, and stores Claude subscription auth in ~/.config/claude-box/state.
Setup model
- Use one generic image named claude-box for every project.
- Run ephemeral containers with --rm.
- Launch from a specific repo root so only that repo is mounted into /workspace.
- Persist Claude Code auth and local CLI state under ~/.config/claude-box/state.
- Do not build one image per project.
Create config directory
Keep the Dockerfile and Claude auth state under ~/.config/claude-box. Create both the state directory and .claude.json before the first run so Docker mounts them as the expected directory and file.
mkdir -p ~/.config/claude-box/state
touch ~/.config/claude-box/state/.claude.json
nano ~/.config/claude-box/DockerfileCreate Dockerfile
FROM archlinux:latest
RUN pacman -Syu --noconfirm nodejs npm git ca-certificates && \
pacman -Scc --noconfirm
RUN npm install -g @anthropic-ai/claude-code
ARG UID=1000
ARG GID=1000
RUN groupadd -g ${GID} claude && \
useradd -m -u ${UID} -g ${GID} claude
USER claude
WORKDIR /workspace
ENTRYPOINT ["claude"]Check Docker
Confirm the Docker daemon is running before building the image. If Docker group membership was just changed, log out and back in before continuing.
docker ps
sudo systemctl enable --now docker
sudo usermod -aG docker $USERBuild the image
Build the image with the host UID and GID so files written inside the mounted repo have clean ownership on the host.
docker build \
--build-arg UID=$(id -u) \
--build-arg GID=$(id -g) \
-t claude-box \
~/.config/claude-box
docker images | grep claude-boxRun from a project
Start Claude Code from the target repo root. The current directory becomes /workspace inside the container.
cd ~/projects/mlnotebooks
docker run -it --rm \
--cap-drop=ALL \
--security-opt no-new-privileges \
--user "$(id -u):$(id -g)" \
-v "$(pwd)":/workspace \
-v ~/.config/claude-box/state:/home/claude/.claude \
-v ~/.config/claude-box/state/.claude.json:/home/claude/.claude.json \
claude-box- On first run, choose the Claude account with subscription login option.
- Open the printed URL in the browser, authorize, then return to the terminal.
- Later runs reuse the token and local Claude state from ~/.config/claude-box/state.
Keep billing on subscription
- Use the Claude account subscription login flow for Pro or Max.
- Do not set ANTHROPIC_API_KEY for this workflow.
- Decline API-credit prompts if the goal is to stay within plan allocation.
- Use /status inside Claude Code to inspect current usage mode.
echo "$ANTHROPIC_API_KEY"Security boundary
- The mount is the workspace boundary. Only launch from the repo Claude should access.
- Do not launch from ~/projects because that exposes every project under it.
- The container runs as the host UID and GID, not as root.
- --cap-drop=ALL and --security-opt no-new-privileges reduce container privileges.
- Anything mounted is writable by Claude Code, including .env files and development secrets in the repo.
- Container isolation is not the same as a VM. Use a VM if the threat model requires a separate kernel.
Add project context
Put project-specific instructions in CLAUDE.md at the repo root. Claude Code reads it automatically, and it can be version-controlled with the project.
cd ~/projects/mlnotebooks
nano CLAUDE.md- Use CLAUDE.md for conventions, project commands, boundaries, and recurring lessons.
- A per-project .claude directory may be created by Claude Code and can stay with the repo if the settings are meant to travel with it.
- The same claude-box image stays generic. Project focus comes from the mount.
Know the limits
This standalone container is for reading and editing code. It does not control the host Docker daemon, join the project Compose network, or reach services by Compose DNS names.
- It cannot run docker compose unless Docker access is deliberately added.
- It cannot reach services such as backend, mysql, redis, or qdrant by Compose service name.
- To operate the stack later, add a claude service to the project docker-compose.yml and join the existing network.
- Do not mount /var/run/docker.sock into this container unless host-level Docker control is explicitly intended.
Troubleshoot
| Issue | Fix |
|---|---|
| Blank screen or silent hang | Omit --read-only, or add writable tmpfs mounts for /tmp, /home/claude/.cache, and /home/claude/.npm |
| Host files get wrong ownership | Rebuild with --build-arg UID=$(id -u) and --build-arg GID=$(id -g) |
| Docker permission denied | Add the user to the docker group, then log out and back in |
| Claude asks about API credits | Confirm ANTHROPIC_API_KEY is empty and use the subscription login flow |
| Claude asks to log in every run | Confirm both /home/claude/.claude and /home/claude/.claude.json are mounted from ~/.config/claude-box/state |
Update later
Rebuild the generic image when Claude Code updates or when setting up a new machine.
docker build \
--build-arg UID=$(id -u) \
--build-arg GID=$(id -g) \
-t claude-box \
~/.config/claude-boxFile locations
- Generic Dockerfile: ~/.config/claude-box/Dockerfile
- Claude auth and CLI state directory: ~/.config/claude-box/state
- Claude local state file: ~/.config/claude-box/state/.claude.json
- Mounted project inside container: /workspace
- Project context file: CLAUDE.md
- Optional project settings: .claude/
- Docker image name: claude-box