From 83029b56d2a700e813c9985c3e4261a6262c0e1d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 00:22:39 +0000 Subject: [PATCH 1/4] Optimize Docker layering with BuildKit cache mounts Add persistent cache mounts for all package managers (dnf, Homebrew, Go, pip, npm) so downloads survive layer invalidation. The daily dnf update no longer triggers a full re-download of every package - only changed packages are fetched. Also adds registry-based build cache as a fallback for when the GHA cache is evicted, ensuring CI builds always have layer cache available. --- .github/workflows/build-ai-dev.yml | 8 ++++-- .github/workflows/build-nvim-dev.yml | 8 ++++-- dot_files/ai-dev/Containerfile | 5 +++- dot_files/nvim/Containerfile | 40 ++++++++++++++++++---------- dot_files/nvim/Justfile | 8 +++--- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build-ai-dev.yml b/.github/workflows/build-ai-dev.yml index 9256c6b..54b0c75 100644 --- a/.github/workflows/build-ai-dev.yml +++ b/.github/workflows/build-ai-dev.yml @@ -68,8 +68,12 @@ jobs: 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 + cache-from: | + type=gha + type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: | + type=gha,mode=max + type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max - name: Generate build summary if: github.event_name != 'pull_request' diff --git a/.github/workflows/build-nvim-dev.yml b/.github/workflows/build-nvim-dev.yml index a14ef4f..750316d 100644 --- a/.github/workflows/build-nvim-dev.yml +++ b/.github/workflows/build-nvim-dev.yml @@ -68,8 +68,12 @@ jobs: 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 + cache-from: | + type=gha + type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: | + type=gha,mode=max + type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max - name: Generate build summary if: github.event_name != 'pull_request' diff --git a/dot_files/ai-dev/Containerfile b/dot_files/ai-dev/Containerfile index 689789d..ac02fa0 100644 --- a/dot_files/ai-dev/Containerfile +++ b/dot_files/ai-dev/Containerfile @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:1 + # AI Development Environment (Sandboxed Podman Container) # Pre-built image with Claude Code and Gemini CLI # @@ -18,7 +20,8 @@ RUN curl -fsSL https://claude.ai/install.sh | bash # ============================================================================= # LAYER 2: Gemini CLI via npm # ============================================================================= -RUN npm install -g @google/gemini-cli +RUN --mount=type=cache,target=/home/linuxbrew/.npm/_cacache,uid=1001,gid=1001 \ + npm install -g @google/gemini-cli # ============================================================================= # LAYER 3: Entrypoint script (sets PATH, execs command) diff --git a/dot_files/nvim/Containerfile b/dot_files/nvim/Containerfile index 0a1e6e8..10532f3 100644 --- a/dot_files/nvim/Containerfile +++ b/dot_files/nvim/Containerfile @@ -1,3 +1,5 @@ +# syntax=docker/dockerfile:1 + # Neovim Development Environment for Distrobox # Pre-built image with all LSPs, formatters, linters, and tools # @@ -8,8 +10,10 @@ FROM registry.fedoraproject.org/fedora-toolbox:43 # ============================================================================= # LAYER 1: System packages +# Cache mount ensures RPM downloads persist across layer-busting rebuilds # ============================================================================= -RUN dnf update -y && dnf install -y \ +RUN --mount=type=cache,target=/var/cache/dnf \ + dnf update -y && dnf install -y \ # Build essentials gcc \ gcc-c++ \ @@ -50,9 +54,7 @@ RUN dnf update -y && dnf install -y \ gettext \ # Locale support langpacks-en \ - glibc-langpack-en \ - && dnf clean all \ - && rm -rf /var/cache/dnf + glibc-langpack-en ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8 @@ -60,7 +62,7 @@ ENV LC_ALL=en_US.UTF-8 # ============================================================================= # LAYER 2: Homebrew installation with non-root user # ============================================================================= -RUN useradd -m -s /bin/bash linuxbrew \ +RUN useradd -m -s /bin/bash -u 1001 linuxbrew \ && git clone https://github.com/Homebrew/brew /home/linuxbrew/.linuxbrew/Homebrew \ && mkdir -p /home/linuxbrew/.linuxbrew/bin \ && ln -s ../Homebrew/bin/brew /home/linuxbrew/.linuxbrew/bin/ \ @@ -69,18 +71,21 @@ RUN useradd -m -s /bin/bash linuxbrew \ ENV PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}" ENV HOMEBREW_NO_AUTO_UPDATE=1 ENV HOMEBREW_NO_ANALYTICS=1 +ENV HOMEBREW_CACHE="/home/linuxbrew/.cache/Homebrew" # Switch to linuxbrew user for all brew operations USER linuxbrew WORKDIR /home/linuxbrew # Update Homebrew -RUN brew update +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew update # ============================================================================= # LAYER 3: Core tools via Homebrew # ============================================================================= -RUN brew install \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew install \ neovim \ ripgrep \ fd \ @@ -98,7 +103,8 @@ RUN brew install \ # ============================================================================= # LAYER 4: Languages via Homebrew # ============================================================================= -RUN brew install \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew install \ go \ python@3.12 \ node \ @@ -109,7 +115,8 @@ RUN brew install \ # ============================================================================= # LAYER 5: Formatters and linters via Homebrew # ============================================================================= -RUN brew install \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew install \ stylua \ prettier \ shfmt \ @@ -120,7 +127,8 @@ RUN brew install \ # ============================================================================= # LAYER 6: Infrastructure tools via Homebrew # ============================================================================= -RUN brew install \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew install \ terraform \ tflint \ helm \ @@ -134,7 +142,8 @@ ENV CARGO_HOME="/home/linuxbrew/.cargo" ENV RUSTUP_HOME="/home/linuxbrew/.rustup" ENV PATH="${CARGO_HOME}/bin:${PATH}" -RUN brew install rustup-init \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 \ + brew install rustup-init \ && rustup-init -y --default-toolchain stable \ && . ${CARGO_HOME}/env \ && rustup component add rustfmt clippy rust-analyzer @@ -145,7 +154,8 @@ RUN brew install rustup-init \ ENV GOPATH="/home/linuxbrew/go" ENV PATH="${GOPATH}/bin:${PATH}" -RUN go install golang.org/x/tools/gopls@latest \ +RUN --mount=type=cache,target=/home/linuxbrew/go/pkg/mod/cache,uid=1001,gid=1001 \ + go install golang.org/x/tools/gopls@latest \ && go install github.com/go-delve/delve/cmd/dlv@latest \ && go install mvdan.cc/gofumpt@latest \ && go install golang.org/x/tools/cmd/goimports@latest \ @@ -154,7 +164,8 @@ RUN go install golang.org/x/tools/gopls@latest \ # ============================================================================= # LAYER 9: Python tools (as linuxbrew user, using brew's python) # ============================================================================= -RUN pip3 install --break-system-packages \ +RUN --mount=type=cache,target=/home/linuxbrew/.cache/pip,uid=1001,gid=1001 \ + pip3 install --break-system-packages \ pynvim \ ruff \ black \ @@ -167,7 +178,8 @@ RUN pip3 install --break-system-packages \ ENV NPM_CONFIG_PREFIX="/home/linuxbrew/.npm-global" ENV PATH="${NPM_CONFIG_PREFIX}/bin:${PATH}" -RUN mkdir -p ${NPM_CONFIG_PREFIX} \ +RUN --mount=type=cache,target=/home/linuxbrew/.npm/_cacache,uid=1001,gid=1001 \ + mkdir -p ${NPM_CONFIG_PREFIX} \ && npm install -g \ neovim \ typescript \ diff --git a/dot_files/nvim/Justfile b/dot_files/nvim/Justfile index a85990d..c05b841 100644 --- a/dot_files/nvim/Justfile +++ b/dot_files/nvim/Justfile @@ -13,15 +13,15 @@ default: # Image Building # ============================================================================= -# Build the container image locally +# Build the container image locally (uses layer cache + download caches) build: @echo "Building nvim-dev image locally..." - podman build -t {{local_image}} . + podman build --layers -t {{local_image}} . @echo "Done! Image: {{local_image}}" -# Build without cache +# Rebuild all layers (download caches still persist via cache mounts) build-no-cache: - @echo "Building nvim-dev image (no cache)..." + @echo "Building nvim-dev image (no layer cache, download caches preserved)..." podman build --no-cache -t {{local_image}} . @echo "Done! Image: {{local_image}}" From c5caf61c9943e8293d0ed17a582d192d3960743a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 00:54:01 +0000 Subject: [PATCH 2/4] Fix ai-dev build: remove npm cache mount with wrong UID The ai-dev container inherits from the published nvim-dev:latest where linuxbrew has a system-assigned UID (not 1001). The cache mount with uid=1001 created a directory the linuxbrew user couldn't write to. For a single package install the cache mount adds negligible benefit, so remove it entirely. --- dot_files/ai-dev/Containerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dot_files/ai-dev/Containerfile b/dot_files/ai-dev/Containerfile index ac02fa0..43d1c11 100644 --- a/dot_files/ai-dev/Containerfile +++ b/dot_files/ai-dev/Containerfile @@ -20,8 +20,7 @@ RUN curl -fsSL https://claude.ai/install.sh | bash # ============================================================================= # LAYER 2: Gemini CLI via npm # ============================================================================= -RUN --mount=type=cache,target=/home/linuxbrew/.npm/_cacache,uid=1001,gid=1001 \ - npm install -g @google/gemini-cli +RUN npm install -g @google/gemini-cli # ============================================================================= # LAYER 3: Entrypoint script (sets PATH, execs command) From 57ef8691437a8691c88ba3b8b23d9cb01cfc41f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 01:04:47 +0000 Subject: [PATCH 3/4] Remove registry cache: BuildKit fails build on auth errors The type=registry cache-to requires push auth which isn't available on PR builds (login step is skipped). Unlike GHA cache, BuildKit's registry exporter fails the entire build rather than degrading gracefully. The GHA cache with mode=max already caches all intermediate layers and is sufficient for the current workflow. --- .github/workflows/build-ai-dev.yml | 8 ++------ .github/workflows/build-nvim-dev.yml | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-ai-dev.yml b/.github/workflows/build-ai-dev.yml index 54b0c75..9256c6b 100644 --- a/.github/workflows/build-ai-dev.yml +++ b/.github/workflows/build-ai-dev.yml @@ -68,12 +68,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=gha - type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - cache-to: | - type=gha,mode=max - type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + cache-from: type=gha + cache-to: type=gha,mode=max - name: Generate build summary if: github.event_name != 'pull_request' diff --git a/.github/workflows/build-nvim-dev.yml b/.github/workflows/build-nvim-dev.yml index 750316d..a14ef4f 100644 --- a/.github/workflows/build-nvim-dev.yml +++ b/.github/workflows/build-nvim-dev.yml @@ -68,12 +68,8 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=gha - type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - cache-to: | - type=gha,mode=max - type=registry,ref=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max + cache-from: type=gha + cache-to: type=gha,mode=max - name: Generate build summary if: github.event_name != 'pull_request' From 087b70a5ad6e33d6c4ca6a0f4fabe41239b13d0c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Jan 2026 01:19:59 +0000 Subject: [PATCH 4/4] Fix cache mount parent dirs: pre-create with correct ownership BuildKit cache mounts create intermediate parent directories as root. This prevented go install from writing to $GOPATH/bin and npm from writing to ~/.npm (for locks/logs), since both ran as linuxbrew. Pre-create these directory trees before the cache-mounted layers so they're owned by linuxbrew (uid 1001). --- dot_files/nvim/Containerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dot_files/nvim/Containerfile b/dot_files/nvim/Containerfile index 10532f3..f5289b9 100644 --- a/dot_files/nvim/Containerfile +++ b/dot_files/nvim/Containerfile @@ -150,10 +150,13 @@ RUN --mount=type=cache,target=/home/linuxbrew/.cache/Homebrew,uid=1001,gid=1001 # ============================================================================= # LAYER 8: Go tools (as linuxbrew user) +# Pre-create GOPATH so the cache mount doesn't leave parents root-owned # ============================================================================= ENV GOPATH="/home/linuxbrew/go" ENV PATH="${GOPATH}/bin:${PATH}" +RUN mkdir -p ${GOPATH}/bin ${GOPATH}/pkg/mod/cache + RUN --mount=type=cache,target=/home/linuxbrew/go/pkg/mod/cache,uid=1001,gid=1001 \ go install golang.org/x/tools/gopls@latest \ && go install github.com/go-delve/delve/cmd/dlv@latest \ @@ -174,13 +177,15 @@ RUN --mount=type=cache,target=/home/linuxbrew/.cache/pip,uid=1001,gid=1001 \ # ============================================================================= # LAYER 10: Node.js/npm tools (as linuxbrew user) +# Pre-create .npm so the cache mount doesn't leave parent root-owned # ============================================================================= ENV NPM_CONFIG_PREFIX="/home/linuxbrew/.npm-global" ENV PATH="${NPM_CONFIG_PREFIX}/bin:${PATH}" +RUN mkdir -p /home/linuxbrew/.npm ${NPM_CONFIG_PREFIX} + RUN --mount=type=cache,target=/home/linuxbrew/.npm/_cacache,uid=1001,gid=1001 \ - mkdir -p ${NPM_CONFIG_PREFIX} \ - && npm install -g \ + npm install -g \ neovim \ typescript \ typescript-language-server \