diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8280c706e6..ba9c25e400 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -44,6 +44,13 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" echo "Stamping build version: ${version:-}" + # ISO week stamp for the Dockerfile's tool layer: the layer caches + # across commits and re-pulls the @latest CLI tools when the week rolls + # over, instead of on every build. + - name: Compute tool cache epoch + id: tools-epoch + run: echo "epoch=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" + - name: Setup pnpm uses: pnpm/action-setup@v6 with: @@ -149,10 +156,16 @@ jobs: build-args: | PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }} PAPERCLIP_BUILD_COMMIT=${{ github.sha }} + CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }} platforms: linux/amd64,linux/arm64 push: true - cache-from: type=gha - cache-to: type=gha,mode=max + # Registry-backed BuildKit cache instead of type=gha: the Actions + # cache is capped at 10GB per repo, and two multi-arch mode=max jobs + # evict each other, so most builds ran effectively cold. The cache + # ref lives in ghcr next to the image and is written only by this + # workflow (docker.yml runs on master/tag pushes, never on PRs). + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache + cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -188,6 +201,13 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" echo "Stamping build version: ${version:-}" + # ISO week stamp for the Dockerfile's tool layer: the layer caches + # across commits and re-pulls the @latest CLI tools when the week rolls + # over, instead of on every build. + - name: Compute tool cache epoch + id: tools-epoch + run: echo "epoch=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" + - name: Setup pnpm uses: pnpm/action-setup@v6 with: @@ -297,13 +317,17 @@ jobs: CLOUD_BUNDLED_PLUGINS=daytona PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }} PAPERCLIP_BUILD_COMMIT=${{ github.sha }} + CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }} # amd64 only, unlike the self-hosted image above: the cloud variant # is consumed exclusively by managed-deployment hosts, which run # amd64. The QEMU-emulated arm64 half dominated this job's wall # clock, and dropping it roughly halves time-to-deployable-image. platforms: linux/amd64 push: true - cache-from: type=gha - cache-to: type=gha,mode=max + # Registry-backed BuildKit cache, separate ref from the self-hosted + # job so the two parallel builds never clobber each other's cache + # manifest (see the rationale on the job above). + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-cloud + cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-cloud,mode=max tags: ${{ steps.meta-cloud.outputs.tags }} labels: ${{ steps.meta-cloud.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 2dd12f5b9c..2769078b66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,9 +67,17 @@ ARG PAPERCLIP_BUILD_VERSION="" # falls back to PAPERCLIP_BUILD_COMMIT when git is unavailable, which feeds the # /api/health `commit` field that deploy tooling verifies. Empty locally. ARG PAPERCLIP_BUILD_COMMIT="" +# Refreshes the tool layer below when it changes (CI stamps an ISO week, so +# the @latest CLI tools advance weekly). Without it the cached layer would +# freeze the tools until an unrelated cache bust. +ARG CLI_TOOLS_CACHE_EPOCH="" WORKDIR /app -COPY --chown=node:node --from=build /app /app -RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai @google/gemini-cli@latest \ +# Tool and OS layer BEFORE the app copy: it references nothing from /app, and +# the app copy changes on every commit — ordered the other way around, this +# (the single most expensive layer: four CLI toolchains + apt, per arch) can +# never hit the layer cache and rebuilds on every build. +RUN echo "cli-tools-epoch: ${CLI_TOOLS_CACHE_EPOCH}" \ + && npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai @google/gemini-cli@latest \ && apt-get update \ && apt-get install -y --no-install-recommends openssh-client jq \ && rm -rf /var/lib/apt/lists/* \ @@ -79,6 +87,8 @@ RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/cod COPY scripts/docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh +COPY --chown=node:node --from=build /app /app + ENV NODE_ENV=production \ HOME=/paperclip \ HOST=0.0.0.0 \