From 806a5eec95af6506e8fee20157a9e9317f45e8e2 Mon Sep 17 00:00:00 2001 From: Geoff Johnson Date: Tue, 10 Mar 2026 22:42:09 -0700 Subject: [PATCH 1/3] feat(docker): add Dockerfile and CI pipeline for Claude Code execution image Create docker/claude-code/Dockerfile based on node:22-slim with: - Claude Code CLI installed via npm - Common dev tools (git, curl, jq, openssh-client) - Non-root 'agent' user (UID 1000) matching DockerBackend defaults - /workspace working directory with safe.directory configured - claude entrypoint, no secrets baked in Add GitHub Actions workflow (.github/workflows/docker-claude-code.yml): - Triggers on Dockerfile changes, releases, and manual dispatch - Builds multi-platform images (linux/amd64, linux/arm64) - Pushes to ghcr.io/geoffjay/agentd-claude with latest, SHA, and semver tags - Uses buildx layer caching for fast rebuilds Add Makefile with docker-build-claude target and standard Rust targets. Add .dockerignore to keep build context minimal. Closes #286 Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 42 ++++++++++++ .github/workflows/docker-claude-code.yml | 78 +++++++++++++++++++++ Makefile | 47 +++++++++++++ docker/claude-code/Dockerfile | 65 ++++++++++++++++++ docker/claude-code/README.md | 86 ++++++++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-claude-code.yml create mode 100644 Makefile create mode 100644 docker/claude-code/Dockerfile create mode 100644 docker/claude-code/README.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..216f02d7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# Build artifacts +target/ +*.rlib +*.d + +# Version control +.git/ +.gitignore + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Environment and secrets +.env +.env.* +secrets/ + +# Documentation build output +docs/book/ + +# CI/CD +.github/ + +# Node modules (if any local tooling) +node_modules/ + +# Test databases and temp files +*.db +*.db-shm +*.db-wal + +# UI build artifacts +ui/node_modules/ +ui/dist/ diff --git a/.github/workflows/docker-claude-code.yml b/.github/workflows/docker-claude-code.yml new file mode 100644 index 00000000..17a68771 --- /dev/null +++ b/.github/workflows/docker-claude-code.yml @@ -0,0 +1,78 @@ +name: Docker — Claude Code Image + +on: + push: + branches: [main] + paths: + - "docker/claude-code/**" + - ".github/workflows/docker-claude-code.yml" + pull_request: + branches: [main] + paths: + - "docker/claude-code/**" + - ".github/workflows/docker-claude-code.yml" + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: geoffjay/agentd-claude + +jobs: + build: + name: Build & Push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU (for multi-platform builds) + uses: docker/setup-qemu-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # latest on main branch pushes + type=raw,value=latest,enable={{is_default_branch}} + # short SHA on every build + type=sha,prefix=sha- + # semver tags on releases (v1.2.3 → 1.2.3, 1.2, 1) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: docker/claude-code + file: docker/claude-code/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Verify image (amd64) + if: github.event_name != 'pull_request' + run: | + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7} + docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7} --version diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9672bc42 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# agentd — top-level build automation +# +# Usage: +# make help Show available targets +# make build Build the Rust workspace +# make test Run all tests +# make docker-build-claude Build the Claude Code Docker image locally + +.PHONY: help build test clippy fmt docker-build-claude docker-run-claude + +# Default image name — matches the DEFAULT_IMAGE constant in crates/wrap/src/docker.rs +CLAUDE_IMAGE ?= agentd-claude:latest + +help: ## Show this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-24s\033[0m %s\n", $$1, $$2}' + +# ── Rust ───────────────────────────────────────────────────────────── + +build: ## Build the Rust workspace + cargo build --workspace + +test: ## Run all workspace tests + cargo test --workspace + +clippy: ## Run clippy lints + cargo clippy --workspace -- -D warnings + +fmt: ## Check formatting + cargo fmt --all -- --check + +fmt-fix: ## Auto-fix formatting + cargo fmt --all + +# ── Docker ─────────────────────────────────────────────────────────── + +docker-build-claude: ## Build the Claude Code agent Docker image locally + docker build -t $(CLAUDE_IMAGE) docker/claude-code/ + +docker-build-claude-multiarch: ## Build multi-platform Claude Code image (requires buildx) + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t $(CLAUDE_IMAGE) \ + docker/claude-code/ + +docker-run-claude: ## Run claude --version in the agent image (smoke test) + docker run --rm $(CLAUDE_IMAGE) --version diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile new file mode 100644 index 00000000..6d3656d4 --- /dev/null +++ b/docker/claude-code/Dockerfile @@ -0,0 +1,65 @@ +# syntax=docker/dockerfile:1 +# +# Claude Code agent execution image for agentd. +# +# This image provides a minimal environment for running the Claude Code CLI +# inside Docker containers managed by the agentd orchestrator's DockerBackend. +# +# Build: +# docker build -t agentd-claude:latest docker/claude-code/ +# +# Run (requires ANTHROPIC_API_KEY): +# docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" agentd-claude:latest --version +# +# Usage with agentd: +# The orchestrator's DockerBackend creates containers from this image +# automatically. API keys are passed as environment variables at runtime — +# nothing secret is baked into the image. + +FROM node:22-slim AS base + +# ── System dependencies ─────────────────────────────────────────────── +# Install common dev tools needed by Claude Code and agent workflows. +# - git: repository operations, diffs, commits +# - ca-certificates: HTTPS connections to APIs and registries +# - curl: health checks, API calls, downloading tools +# - jq: JSON processing in shell scripts +# - openssh-client: git operations over SSH +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + jq \ + openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# ── Claude Code CLI ────────────────────────────────────────────────── +# Install globally so it's available on PATH for all users. +RUN npm install -g @anthropic-ai/claude-code && \ + npm cache clean --force + +# ── Non-root user ──────────────────────────────────────────────────── +# Create a dedicated 'agent' user. The UID/GID 1000 matches the default +# user on most Linux hosts and the user set by DockerBackend (1000:1000). +RUN groupadd --gid 1000 agent && \ + useradd --uid 1000 --gid agent --create-home --shell /bin/bash agent + +# ── Git configuration ──────────────────────────────────────────────── +# Set safe.directory so git works in bind-mounted /workspace regardless +# of ownership. Also set a default identity for commits made by agents. +RUN git config --system safe.directory /workspace && \ + git config --system user.email "agent@agentd.local" && \ + git config --system user.name "agentd agent" + +# ── Workspace ──────────────────────────────────────────────────────── +RUN mkdir -p /workspace && chown agent:agent /workspace +WORKDIR /workspace + +# ── Runtime ────────────────────────────────────────────────────────── +USER agent + +# Verify the CLI is installed and accessible. +RUN claude --version + +ENTRYPOINT ["claude"] diff --git a/docker/claude-code/README.md b/docker/claude-code/README.md new file mode 100644 index 00000000..ca804a5a --- /dev/null +++ b/docker/claude-code/README.md @@ -0,0 +1,86 @@ +# Claude Code Agent Image + +Docker image for running Claude Code CLI agents inside containers managed by +the agentd orchestrator's `DockerBackend`. + +## Quick Start + +```bash +# Build locally +make docker-build-claude + +# Or build directly +docker build -t agentd-claude:latest docker/claude-code/ + +# Verify +docker run --rm agentd-claude:latest --version + +# Run with API key +docker run --rm \ + -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ + -v "$(pwd):/workspace" \ + agentd-claude:latest --version +``` + +## What's Included + +| Component | Purpose | +|-----------|---------| +| Node.js 22 (slim) | Runtime for Claude Code CLI | +| `@anthropic-ai/claude-code` | The Claude Code CLI itself | +| `git` | Repository operations | +| `curl` | HTTP requests, health checks | +| `jq` | JSON processing | +| `openssh-client` | Git over SSH | +| `ca-certificates` | TLS certificate validation | + +## Security + +- **Non-root**: Runs as `agent` (UID 1000) by default +- **No secrets**: API keys are passed at runtime via environment variables +- **Minimal base**: Uses `node:22-slim` to reduce attack surface + +## Customization + +### Adding tools + +Create a derived Dockerfile: + +```dockerfile +FROM ghcr.io/geoffjay/agentd-claude:latest + +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* +USER agent +``` + +### Using a different base image + +Fork this Dockerfile and change the `FROM` line. The key requirements are: +- Node.js (for the Claude Code CLI) +- A non-root user with UID 1000 +- `/workspace` as the working directory + +## Image Tags + +| Tag | Description | +|-----|-------------| +| `latest` | Latest build from `main` branch | +| `sha-` | Pinned to a specific git commit | +| `v1.2.3` | Pinned to a semver release tag | + +## How It Works with agentd + +The `DockerBackend` in the orchestrator creates containers from this image: + +1. The orchestrator calls `docker create` with this image +2. The host project directory is bind-mounted to `/workspace` +3. API keys are passed as environment variables +4. The `NetworkPolicy` controls container networking +5. Claude Code connects back to the orchestrator via WebSocket + +The container's entrypoint is `claude`, and the orchestrator passes additional +arguments (like `--sdk-url`, `--output-format stream-json`) via `docker exec`. From 0787f7f6eb96917651575a835ab2f45fcf05b22e Mon Sep 17 00:00:00 2001 From: Geoff Johnson Date: Tue, 10 Mar 2026 22:48:13 -0700 Subject: [PATCH 2/3] fix(docker): rename existing node user instead of creating new UID 1000 The node:22-slim base image already has a `node` user at UID 1000. Use groupmod/usermod to rename it to `agent` instead of groupadd/useradd which fails with exit code 4 (UID already exists). Co-Authored-By: Claude Opus 4.6 --- docker/claude-code/Dockerfile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile index 6d3656d4..03bce30d 100644 --- a/docker/claude-code/Dockerfile +++ b/docker/claude-code/Dockerfile @@ -40,10 +40,13 @@ RUN npm install -g @anthropic-ai/claude-code && \ npm cache clean --force # ── Non-root user ──────────────────────────────────────────────────── -# Create a dedicated 'agent' user. The UID/GID 1000 matches the default -# user on most Linux hosts and the user set by DockerBackend (1000:1000). -RUN groupadd --gid 1000 agent && \ - useradd --uid 1000 --gid agent --create-home --shell /bin/bash agent +# The node:22-slim base image already has a `node` user at UID 1000. +# Rename it to `agent` for clarity and to match DockerBackend's default +# user (1000:1000). Using usermod/groupmod avoids the "UID already +# exists" error from useradd. +RUN groupmod --new-name agent node && \ + usermod --login agent --home /home/agent --move-home --comment "agentd agent" node && \ + mkdir -p /home/agent && chown agent:agent /home/agent # ── Git configuration ──────────────────────────────────────────────── # Set safe.directory so git works in bind-mounted /workspace regardless From c0af523f441cb200545627404e00ebc22b8dd743 Mon Sep 17 00:00:00 2001 From: Geoff Johnson Date: Wed, 11 Mar 2026 07:11:15 -0700 Subject: [PATCH 3/3] fix: address code review findings for Dockerfile and CI - Replace ENTRYPOINT with CMD to prevent `claude claude` invocation when DockerBackend sets cmd via create_container (blocking issue) - Remove unused `AS base` alias from single-stage build - Pin @anthropic-ai/claude-code version via build ARG for reproducibility - Remove redundant mkdir/chown after usermod --move-home - Add fmt-fix and docker-build-claude-multiarch to .PHONY in Makefile - Add comment noting arm64 verify gap in CI workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/docker-claude-code.yml | 3 +++ Makefile | 2 +- docker/claude-code/Dockerfile | 12 +++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-claude-code.yml b/.github/workflows/docker-claude-code.yml index 17a68771..be487335 100644 --- a/.github/workflows/docker-claude-code.yml +++ b/.github/workflows/docker-claude-code.yml @@ -71,6 +71,9 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + # NOTE: This only verifies the amd64 variant. The arm64 layer is built + # and pushed but not smoke-tested here because ubuntu-latest is amd64. + # A future improvement could add a QEMU-based arm64 verify step. - name: Verify image (amd64) if: github.event_name != 'pull_request' run: | diff --git a/Makefile b/Makefile index 9672bc42..545a846f 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # make test Run all tests # make docker-build-claude Build the Claude Code Docker image locally -.PHONY: help build test clippy fmt docker-build-claude docker-run-claude +.PHONY: help build test clippy fmt fmt-fix docker-build-claude docker-build-claude-multiarch docker-run-claude # Default image name — matches the DEFAULT_IMAGE constant in crates/wrap/src/docker.rs CLAUDE_IMAGE ?= agentd-claude:latest diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile index 03bce30d..fa911f70 100644 --- a/docker/claude-code/Dockerfile +++ b/docker/claude-code/Dockerfile @@ -16,7 +16,7 @@ # automatically. API keys are passed as environment variables at runtime — # nothing secret is baked into the image. -FROM node:22-slim AS base +FROM node:22-slim # ── System dependencies ─────────────────────────────────────────────── # Install common dev tools needed by Claude Code and agent workflows. @@ -36,7 +36,10 @@ RUN apt-get update && \ # ── Claude Code CLI ────────────────────────────────────────────────── # Install globally so it's available on PATH for all users. -RUN npm install -g @anthropic-ai/claude-code && \ +# Pin the version for reproducible builds. Update deliberately or via +# Renovate / Dependabot when a new release is needed. +ARG CLAUDE_CODE_VERSION=0.2.70 +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} && \ npm cache clean --force # ── Non-root user ──────────────────────────────────────────────────── @@ -45,8 +48,7 @@ RUN npm install -g @anthropic-ai/claude-code && \ # user (1000:1000). Using usermod/groupmod avoids the "UID already # exists" error from useradd. RUN groupmod --new-name agent node && \ - usermod --login agent --home /home/agent --move-home --comment "agentd agent" node && \ - mkdir -p /home/agent && chown agent:agent /home/agent + usermod --login agent --home /home/agent --move-home --comment "agentd agent" node # ── Git configuration ──────────────────────────────────────────────── # Set safe.directory so git works in bind-mounted /workspace regardless @@ -65,4 +67,4 @@ USER agent # Verify the CLI is installed and accessible. RUN claude --version -ENTRYPOINT ["claude"] +CMD ["claude"]