From c3ab14fa301fb669fa5101efd95d60f482ac3dee Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sat, 4 Apr 2026 16:24:05 +0800 Subject: [PATCH] perf: split claude-dev into base and runtime images for faster builds - Create Dockerfile.base with all tools and dependencies (built once) - Slim down Dockerfile to just copy scripts from base image - CI checks for base image existence, builds if missing - Runtime builds drop from ~17min to ~10sec (only copies scripts) - Base image only needs rebuild when tools/versions change --- .gitea/workflows/deploy-claude-dev-dev.yml | 12 +++++ claude-dev/Dockerfile | 59 +-------------------- claude-dev/Dockerfile.base | 60 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 claude-dev/Dockerfile.base diff --git a/.gitea/workflows/deploy-claude-dev-dev.yml b/.gitea/workflows/deploy-claude-dev-dev.yml index d42e2e3..cd24935 100644 --- a/.gitea/workflows/deploy-claude-dev-dev.yml +++ b/.gitea/workflows/deploy-claude-dev-dev.yml @@ -47,6 +47,18 @@ jobs: if: env.ROLLBACK_ONLY == '0' run: | set -euo pipefail + # Check if base image exists, build if missing + if ! docker image inspect claude-dev-base:latest >/dev/null 2>&1; then + echo "Base image not found, building claude-dev-base:latest..." + DOCKER_BUILDKIT=0 docker build \ + -f claude-dev/Dockerfile.base \ + -t claude-dev-base:latest \ + claude-dev/ + else + echo "Using existing claude-dev-base:latest" + fi + + # Build runtime image (fast, just copies scripts) DOCKER_BUILDKIT=0 docker build \ --cache-from claude-dev:latest \ -t "$CLAUDE_DEV_IMAGE" \ diff --git a/claude-dev/Dockerfile b/claude-dev/Dockerfile index 4b0b22c..e3904d5 100644 --- a/claude-dev/Dockerfile +++ b/claude-dev/Dockerfile @@ -1,61 +1,4 @@ -FROM node:20-bookworm-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - bash \ - ca-certificates \ - curl \ - fd-find \ - gh \ - git \ - jq \ - make \ - openssh-client \ - python3 \ - ripgrep \ - rsync \ - tmux \ - unzip \ - wget \ - zip \ - && rm -rf /var/lib/apt/lists/* - -RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd - -ARG YQ_VERSION=v4.44.6 -RUN wget -q -O /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \ - && chmod +x /usr/local/bin/yq - -ARG TEA_VERSION=0.12.0 -RUN wget -q -O /usr/local/bin/tea "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" \ - && chmod +x /usr/local/bin/tea - -RUN npm install -g @anthropic-ai/claude-code - -RUN python3 - <<'PY' -from pathlib import Path -import re - -path = Path("/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js") -if not path.exists(): - raise SystemExit(f"claude cli not found: {path}") - -content = path.read_text() -pattern = r'let A=U7\(\),q=new URL\(A\.TOKEN_URL\),K=\[`\$\{A\.BASE_API_URL\}/api/hello`,`\$\{q\.origin\}/v1/oauth/hello`\],Y=async\(_\)=>\{try\{let \$=await I8\.get\(_,\{headers:\{"User-Agent":ey\(\)\}\}\);if\(\$\.status!==200\)return\{success:!1,error:`Failed to connect to \$\{new URL\(_\)\.hostname\}: Status \$\{\$\.status\}`\};return\{success:!0\}\}catch\(\$\)\{' -replacement = 'return' -patched, count = re.subn(pattern, replacement, content, count=1) -if count != 1: - raise SystemExit("preflight patch target not found in claude cli; refusing to build") - -path.write_text(patched) - -if re.search(pattern, path.read_text()): - raise SystemExit("preflight patch did not apply cleanly") - -print("Applied Claude preflight bypass patch") -PY +FROM claude-dev-base:latest WORKDIR /repos/nas-tools diff --git a/claude-dev/Dockerfile.base b/claude-dev/Dockerfile.base new file mode 100644 index 0000000..b477a2d --- /dev/null +++ b/claude-dev/Dockerfile.base @@ -0,0 +1,60 @@ +FROM node:20-bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + fd-find \ + gh \ + git \ + jq \ + make \ + openssh-client \ + python3 \ + ripgrep \ + rsync \ + tmux \ + unzip \ + wget \ + zip \ + && rm -rf /var/lib/apt/lists/* + +RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd + +ARG YQ_VERSION=v4.44.6 +RUN wget -q -O /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \ + && chmod +x /usr/local/bin/yq + +ARG TEA_VERSION=0.12.0 +RUN wget -q -O /usr/local/bin/tea "https://gitea.com/gitea/tea/releases/download/v${TEA_VERSION}/tea-${TEA_VERSION}-linux-amd64" \ + && chmod +x /usr/local/bin/tea + +RUN npm install -g @anthropic-ai/claude-code + +RUN python3 - <<'PY' +from pathlib import Path +import re + +path = Path("/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js") +if not path.exists(): + raise SystemExit(f"claude cli not found: {path}") + +content = path.read_text() +pattern = r'let A=U7\(\),q=new URL\(A\.TOKEN_URL\),K=\[`\$\{A\.BASE_API_URL\}/api/hello`,`\$\{q\.origin\}/v1/oauth/hello`\],Y=async\(_\)=>\{try\{let \$=await I8\.get\(_,\{headers:\{"User-Agent":ey\(\)\}\}\);if\(\$\.status!==200\)return\{success:!1,error:`Failed to connect to \$\{new URL\(_\)\.hostname\}: Status \$\{\$\.status\}`\};return\{success:!0\}\}catch\(\$\)\{' +replacement = 'return' +patched, count = re.subn(pattern, replacement, content, count=1) +if count != 1: + raise SystemExit("preflight patch target not found in claude cli; refusing to build") + +path.write_text(patched) + +if re.search(pattern, path.read_text()): + raise SystemExit("preflight patch did not apply cleanly") + +print("Applied Claude preflight bypass patch") +PY + +CMD ["bash"]