diff --git a/Dockerfile b/Dockerfile index a573737e6b..2a9c0d45e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,22 +4,37 @@ FROM node:26.8.1-alpine AS migrations WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 # Keep the bootstrap aligned with package.json packageManager. -RUN apk add --no-cache git \ +# The apk cache is persisted in a BuildKit cache mount so git is not +# re-downloaded on every build. +RUN --mount=type=cache,target=/var/cache/apk \ + apk add --no-cache git \ && npm install -g pnpm@11.25.0 +# The pnpm store is kept in a BuildKit cache mount that persists across builds +# on the builder. This is what stops disk usage from growing unbounded: the +# downloaded dependency store is shared and reused instead of being copied into +# a fresh image layer on every build. Unlike an image layer it is also prunable +# independently, so a hard cap (see ci-deploy.sh) keeps it bounded. +ENV PNPM_HOME=/pnpm PNPM_STORE=/pnpm/store # pnpm-workspace.yaml + .npmrc must be present too: the lockfile records the # overrides from pnpm-workspace.yaml, and --frozen-lockfile rejects a build # where the workspace config is absent (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* .npmrc* ./ -# pnpm fetch: download all deps into $PNPM_STORE first, so only the lockfile -# change (not source changes) invalidates the network-heavy download layer. -RUN pnpm fetch --ignore-scripts -RUN pnpm install --frozen-lockfile --ignore-scripts --offline +# pnpm fetch: download all deps into the shared cache-mounted store. +RUN --mount=type=cache,target=/pnpm \ + pnpm fetch --ignore-scripts +# Install offline from the cache-mounted store; the store itself stays in the +# build cache between builds. +RUN --mount=type=cache,target=/pnpm \ + pnpm install --frozen-lockfile --ignore-scripts --offline COPY . . FROM migrations AS builder ARG NEXT_DEPLOYMENT_ID="unknown" ENV NEXT_DEPLOYMENT_ID="$NEXT_DEPLOYMENT_ID" # Fixture values exist only for this build command; production secrets are runtime-only. -RUN DATABASE_URL="mysql://build:build@127.0.0.1:9/build" \ +# Cache Next.js build output and webpack caches so rebuilds only redo the +# changed parts. +RUN --mount=type=cache,target=/app/.next/cache \ + DATABASE_URL="mysql://build:build@127.0.0.1:9/build" \ HOTEL_NAME="Build fixture" APP_URL="http://localhost:3002" \ AUTH_SECRET="build-fixture-not-for-runtime-use-000000000000" \ pnpm run build diff --git a/scripts/ci-deploy.sh b/scripts/ci-deploy.sh index 01b2d5fe92..0f9261236c 100644 --- a/scripts/ci-deploy.sh +++ b/scripts/ci-deploy.sh @@ -176,5 +176,5 @@ while IFS= read -r tag; do if [ -n "$tagged_image" ] && [ "$tagged_image" != "$previous_image" ]; then docker image rm "$tag" || true; fi fi done < <(docker image ls --format '{{.Repository}}:{{.Tag}}' epicnext-cms) -docker builder prune -af --filter "until=72h" --keep-storage=2g || true +docker builder prune -af --filter "until=72h" --max-used-space=4g || true docker image prune -f --filter "until=168h" || true diff --git a/src/lib/deploy-workflow-contract.test.ts b/src/lib/deploy-workflow-contract.test.ts index 343a733fc1..b459136bd7 100644 --- a/src/lib/deploy-workflow-contract.test.ts +++ b/src/lib/deploy-workflow-contract.test.ts @@ -27,7 +27,7 @@ it("preserves production runtime configuration and recent cache", () => { expect(deploy).toContain("/app/storage"); expect(deploy).not.toContain("--env-file"); expect(deploy).toContain( - 'docker builder prune -af --filter "until=72h" --keep-storage=2g', + 'docker builder prune -af --filter "until=72h" --max-used-space=4g', ); expect(deploy).not.toContain("docker volume prune"); });