Skip to content

Commit edd3c8a

Browse files
feat: Optimize Docker image caching layers (#1139)
## Summary This PR improves Docker build caching so that CI builds are faster and more cache-efficient — both in terms of Docker layer caching and package download caching. --- ## What changed ### `EssentialCSharp.Web/Dockerfile` - **Metadata-first COPY pattern**: copies `.slnx`, `.csproj`, `Directory.*.props`, `global.json`, and `nuget.config` before `dotnet restore` — restore layer is only invalidated when dependencies change, not on every source edit - **Split restore / build / publish**: finer cache granularity; `COPY . .` no longer busts the restore layer - **BuildKit cache mounts**: `--mount=type=cache` for both NuGet packages (`/root/.nuget/packages`) and npm (`/root/.npm`) — skips re-downloading packages when lockfiles haven't changed - **Narrowed frontend COPY inputs**: only `vite.config.js`, `src/`, and `wwwroot/js/` are copied into the frontend stage — backend-only changes no longer invalidate the npm build layer ### `.github/workflows/PR-Build-And-Test.yml` & `Build-Test-And-Deploy.yml` - **Explicit GHA cache scopes**: `essentialcsharpweb-main`, `essentialcsharpweb-pr`, `essentialcsharpweb-deploy` — prevents cross-workflow cache contention - **`reproducible-containers/buildkit-cache-dance`**: bridges BuildKit's daemon-local cache mounts to GitHub Actions cache. Without this, `--mount=type=cache` entries are empty on every CI run (fresh ephemeral runner = fresh BuildKit daemon). With it: - `actions/cache` restores host-side cached data for npm and NuGet - `buildkit-cache-dance` injects that data into BuildKit before the build - After the build, updated cache data is extracted back out and saved **PR vs main branch policy:** - PR workflow → `skip-extraction: true` (reads cache, doesn't write back; avoids polluting canonical cache with feature branches) - Deploy workflow → `skip-extraction: false` (reads and writes; keeps the canonical cache warm) --- ## Caching architecture (after this PR) ``` actions/cache ←→ buildkit-cache-dance ←→ BuildKit cache mounts (/root/.nuget, /root/.npm) ↕ type=gha layer cache (Docker layer snapshots) ``` Both mechanisms now work end-to-end in CI, not just locally.
1 parent 57bcd60 commit edd3c8a

4 files changed

Lines changed: 61 additions & 10 deletions

File tree

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**/.buildkit-cache
2+
**/buildkit-cache
13
**/.classpath
24
**/.dockerignore
35
**/.env

.github/workflows/Build-Test-And-Deploy.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ jobs:
6262

6363
- name: Set up Docker Buildx
6464
uses: docker/setup-buildx-action@v4
65+
id: buildx
66+
67+
- name: Restore BuildKit cache mounts
68+
uses: actions/cache@v5
69+
id: buildkit-cache
70+
with:
71+
path: buildkit-cache
72+
key: buildkit-cache-${{ hashFiles('EssentialCSharp.Web/Dockerfile', 'Directory.Packages.props', '**/*.csproj', 'EssentialCSharp.Web/package-lock.json') }}
73+
restore-keys: buildkit-cache-
74+
75+
- name: Inject BuildKit cache mounts
76+
uses: reproducible-containers/buildkit-cache-dance@v3
77+
with:
78+
builder: ${{ steps.buildx.outputs.name }}
79+
dockerfile: EssentialCSharp.Web/Dockerfile
80+
cache-dir: buildkit-cache
81+
skip-extraction: false
6582

6683
# Only build for dev registry — prod gets the image via az acr import in deploy-production
6784
- name: Build Container Image
@@ -74,8 +91,8 @@ jobs:
7491
secrets: |
7592
"nuget_pat=${{ secrets.AZURE_DEVOPS_PAT }}"
7693
outputs: type=docker,dest=${{ github.workspace }}/essentialcsharpwebimage.tar
77-
cache-from: type=gha
78-
cache-to: type=gha,mode=max
94+
cache-from: type=gha,scope=essentialcsharpweb-main
95+
cache-to: type=gha,mode=max,scope=essentialcsharpweb-main
7996

8097
- name: Upload artifact
8198
uses: actions/upload-artifact@v7

.github/workflows/PR-Build-And-Test.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,32 @@ jobs:
8282

8383
- name: Set up Docker Buildx
8484
uses: docker/setup-buildx-action@v4
85+
id: buildx
86+
87+
- name: Restore BuildKit cache mounts
88+
uses: actions/cache/restore@v5
89+
id: buildkit-cache
90+
with:
91+
path: buildkit-cache
92+
key: buildkit-cache-${{ hashFiles('EssentialCSharp.Web/Dockerfile', 'Directory.Packages.props', '**/*.csproj', 'EssentialCSharp.Web/package-lock.json') }}
93+
restore-keys: buildkit-cache-
94+
95+
- name: Inject BuildKit cache mounts
96+
uses: reproducible-containers/buildkit-cache-dance@v3
97+
with:
98+
builder: ${{ steps.buildx.outputs.name }}
99+
dockerfile: EssentialCSharp.Web/Dockerfile
100+
cache-dir: buildkit-cache
101+
skip-extraction: true
85102

86103
- name: Build Container Image
87104
uses: docker/build-push-action@v7
88105
with:
89106
file: ./EssentialCSharp.Web/Dockerfile
90107
context: .
91108
push: false
92-
cache-from: type=gha
93-
cache-to: type=gha,mode=max
109+
cache-from: |
110+
type=gha,scope=essentialcsharpweb-main
111+
type=gha,scope=essentialcsharpweb-pr
112+
cache-to: type=gha,mode=max,scope=essentialcsharpweb-pr
94113
build-args: ACCESS_TO_NUGET_FEED=false

EssentialCSharp.Web/Dockerfile

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,26 @@ EXPOSE 8081
88
FROM node:26-bookworm-slim AS frontend-build
99
WORKDIR /frontend/EssentialCSharp.Web
1010
COPY EssentialCSharp.Web/package.json EssentialCSharp.Web/package-lock.json ./
11-
RUN npm ci
12-
COPY EssentialCSharp.Web/ ./
11+
RUN --mount=type=cache,id=essentialcsharp-web-npm,target=/root/.npm \
12+
npm ci
13+
COPY EssentialCSharp.Web/vite.config.js ./vite.config.js
14+
COPY EssentialCSharp.Web/src ./src
15+
COPY EssentialCSharp.Web/wwwroot/js ./wwwroot/js
1316
RUN npm run build
1417

1518
FROM mcr.microsoft.com/dotnet/sdk:10.0.300 AS build
1619
ARG ACCESS_TO_NUGET_FEED=true
1720
ENV ACCESS_TO_NUGET_FEED=$ACCESS_TO_NUGET_FEED
1821
WORKDIR /src
19-
COPY . .
20-
COPY --from=frontend-build /frontend/EssentialCSharp.Web/wwwroot/dist ./EssentialCSharp.Web/wwwroot/dist
22+
COPY EssentialCSharp.Web.slnx ./
23+
COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json nuget.config ./
24+
COPY EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj ./EssentialCSharp.Chat.Shared/
25+
COPY EssentialCSharp.Chat.Tests/EssentialCSharp.Chat.Tests.csproj ./EssentialCSharp.Chat.Tests/
26+
COPY EssentialCSharp.Chat/EssentialCSharp.Chat.csproj ./EssentialCSharp.Chat/
27+
COPY EssentialCSharp.Web.Tests/EssentialCSharp.Web.Tests.csproj ./EssentialCSharp.Web.Tests/
28+
COPY EssentialCSharp.Web/EssentialCSharp.Web.csproj ./EssentialCSharp.Web/
2129
RUN --mount=type=secret,id=nuget_pat,required=false \
30+
--mount=type=cache,id=essentialcsharp-web-nuget,target=/root/.nuget/packages \
2231
if [ "$ACCESS_TO_NUGET_FEED" = "true" ] && [ ! -s /run/secrets/nuget_pat ]; then \
2332
echo "ERROR: ACCESS_TO_NUGET_FEED=true but nuget_pat secret is missing or empty" >&2; exit 1; \
2433
fi && \
@@ -28,9 +37,13 @@ RUN --mount=type=secret,id=nuget_pat,required=false \
2837
"$(cat /run/secrets/nuget_pat)" > /root/.nuget/NuGet/NuGet.Config; \
2938
fi && \
3039
dotnet restore "EssentialCSharp.Web.slnx" -p:AccessToNugetFeed=$ACCESS_TO_NUGET_FEED && \
31-
dotnet build "EssentialCSharp.Web.slnx" -c Release --no-restore -p:AccessToNugetFeed=$ACCESS_TO_NUGET_FEED -p:ReleaseDateAttribute=True -p:SkipFrontendBuild=true && \
32-
dotnet publish "EssentialCSharp.Web.slnx" -c Release -p:PublishDir=/app/publish -p:UseAppHost=false -p:SkipFrontendBuild=true --no-build && \
3340
rm -f /root/.nuget/NuGet/NuGet.Config
41+
COPY . .
42+
COPY --from=frontend-build /frontend/EssentialCSharp.Web/wwwroot/dist ./EssentialCSharp.Web/wwwroot/dist
43+
RUN --mount=type=cache,id=essentialcsharp-web-nuget,target=/root/.nuget/packages \
44+
dotnet build "EssentialCSharp.Web.slnx" -c Release --no-restore -p:AccessToNugetFeed=$ACCESS_TO_NUGET_FEED -p:ReleaseDateAttribute=True -p:SkipFrontendBuild=true
45+
RUN --mount=type=cache,id=essentialcsharp-web-nuget,target=/root/.nuget/packages \
46+
dotnet publish "EssentialCSharp.Web.slnx" -c Release -p:PublishDir=/app/publish -p:UseAppHost=false -p:SkipFrontendBuild=true --no-build
3447

3548
FROM base AS final
3649
WORKDIR /app

0 commit comments

Comments
 (0)