perf(docker): reuse dependency layers and preserve build caches
This commit is contained in:
1 parent
31691c1c7d
commit
2840ef5d9d
4 files changed
+59
-34
No files matched your search
@@ -86,6 +86,7 @@ jobs:
|
|||||||
# hebben geen outbound internet (npm/pnpm zouden hangen).
|
# hebben geen outbound internet (npm/pnpm zouden hangen).
|
||||||
DOCKER_BUILDKIT=1 docker build \
|
DOCKER_BUILDKIT=1 docker build \
|
||||||
--network=host \
|
--network=host \
|
||||||
|
--progress=plain \
|
||||||
--build-arg NODE_OPTIONS="--max-old-space-size=1536" \
|
--build-arg NODE_OPTIONS="--max-old-space-size=1536" \
|
||||||
--cache-from epicnext-cms:latest \
|
--cache-from epicnext-cms:latest \
|
||||||
-t epicnext-cms:latest .
|
-t epicnext-cms:latest .
|
||||||
@@ -160,18 +161,15 @@ jobs:
|
|||||||
docker compose -f /var/www/atom-nexst/docker-compose.yml up -d --no-build 2>&1 || true
|
docker compose -f /var/www/atom-nexst/docker-compose.yml up -d --no-build 2>&1 || true
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Prune Docker cache
|
- name: Prune old Docker cache
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
# Ruim de self-hosted runner op zodat de Docker daemon niet
|
# Keep recently used layers and cache mounts for subsequent deploys.
|
||||||
# volloopt. Draaiende containers/images (o.a. de net-deployed
|
# The age filter preserves the last 72 hours; keep-storage is a
|
||||||
# epicnext-cms-app) worden nooit geraakt, alleen ongebruikte
|
# cleanup target, not a hard limit on recent cache disk usage.
|
||||||
# images, build-cache, volumes en networks verdwijnen.
|
docker builder prune -af --filter "until=72h" --keep-storage=2g || true
|
||||||
docker image prune -af || true
|
# Only old dangling images; application volumes and networks persist.
|
||||||
docker container prune -f || true
|
docker image prune -f --filter "until=168h" || true
|
||||||
docker volume prune -f || true
|
|
||||||
docker network prune -f || true
|
|
||||||
docker builder prune -af --keep-storage=2g || true
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# E2E smoke against the freshly deployed container.
|
# E2E smoke against the freshly deployed container.
|
||||||
|
|||||||
+14
-17
@@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# EpicNext-CMS — Docker image (Node 26.8.1, multi-package-manager, Next.js standalone)
|
# EpicNext-CMS — Docker image (Node 26.8.1, multi-package-manager, Next.js standalone)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -19,21 +20,19 @@ RUN npm install -g [email protected] yarn
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# First copy only the manifests so dependency layers are cached when using pnpm.
|
# Lockfiles and installer settings are the only inputs to the dependency layer.
|
||||||
# For npm/yarn the full context is copied below before install.
|
# The wildcard supports pnpm-lock.yaml, package-lock.json and yarn.lock.
|
||||||
COPY package.json pnpm-workspace.yaml .npmrc ./
|
COPY package.json *lock* pnpm-workspace.yaml .npmrc ./
|
||||||
|
|
||||||
# Copy the rest of the source (brings in whichever lockfile your project uses).
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# --- Detect package manager & install dependencies ---
|
# --- Detect package manager & install dependencies ---
|
||||||
# Priority: pnpm > yarn > npm
|
# Priority: pnpm > yarn > npm
|
||||||
# Build arg lets the user force a manager; otherwise it is auto-detected.
|
# Build arg lets the user force a manager; otherwise it is auto-detected.
|
||||||
ARG PACKAGE_MANAGER=
|
ARG PACKAGE_MANAGER=
|
||||||
|
|
||||||
RUN if [ "$PACKAGE_MANAGER" = "pnpm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f pnpm-lock.yaml ]; }; then \
|
RUN --mount=type=cache,id=epicnext-pnpm,target=/pnpm/store,sharing=locked \
|
||||||
|
if [ "$PACKAGE_MANAGER" = "pnpm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f pnpm-lock.yaml ]; }; then \
|
||||||
echo ">> Using pnpm" && \
|
echo ">> Using pnpm" && \
|
||||||
pnpm install --frozen-lockfile --ignore-scripts; \
|
pnpm install --frozen-lockfile --ignore-scripts --store-dir=/pnpm/store; \
|
||||||
elif [ "$PACKAGE_MANAGER" = "yarn" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f yarn.lock ]; }; then \
|
elif [ "$PACKAGE_MANAGER" = "yarn" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f yarn.lock ]; }; then \
|
||||||
echo ">> Using yarn" && \
|
echo ">> Using yarn" && \
|
||||||
yarn install --frozen-lockfile --ignore-scripts; \
|
yarn install --frozen-lockfile --ignore-scripts; \
|
||||||
@@ -45,12 +44,16 @@ RUN if [ "$PACKAGE_MANAGER" = "pnpm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f pn
|
|||||||
npm install --ignore-scripts; \
|
npm install --ignore-scripts; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Source changes invalidate compilation, but keep the installed dependencies.
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Build the production bundle.
|
# Build the production bundle.
|
||||||
# The .env file is loaded ONLY inside this RUN layer (not persisted as ENV, so no
|
# The .env file is loaded ONLY inside this RUN layer (not persisted as ENV, so no
|
||||||
# secrets end up in the image) — Next.js needs NEXT_PUBLIC_* + validated build-time
|
# secrets end up in the image) — Next.js needs NEXT_PUBLIC_* + validated build-time
|
||||||
# values (HOTEL_NAME, DATABASE_URL, AUTH_SECRET, ...) at build time.
|
# values (HOTEL_NAME, DATABASE_URL, AUTH_SECRET, ...) at build time.
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN if [ -f .env ]; then set -a && . ./.env && set +a; fi && \
|
RUN --mount=type=cache,id=epicnext-next,target=/app/.next/cache,sharing=locked \
|
||||||
|
if [ -f .env ]; then set -a && . ./.env && set +a; fi && \
|
||||||
if [ "$PACKAGE_MANAGER" = "yarn" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f yarn.lock ]; }; then \
|
if [ "$PACKAGE_MANAGER" = "yarn" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f yarn.lock ]; }; then \
|
||||||
yarn build; \
|
yarn build; \
|
||||||
elif [ "$PACKAGE_MANAGER" = "npm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f package-lock.json ]; }; then \
|
elif [ "$PACKAGE_MANAGER" = "npm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f package-lock.json ]; }; then \
|
||||||
@@ -59,14 +62,8 @@ RUN if [ -f .env ]; then set -a && . ./.env && set +a; fi && \
|
|||||||
pnpm build; \
|
pnpm build; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Prune dev dependencies for the runtime image.
|
# Standalone output already contains the traced runtime dependencies.
|
||||||
RUN if [ "$PACKAGE_MANAGER" = "yarn" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f yarn.lock ]; }; then \
|
# Pruning builder/node_modules here would not shrink the final image.
|
||||||
yarn install --production --ignore-scripts && rm -rf node_modules/.cache; \
|
|
||||||
elif [ "$PACKAGE_MANAGER" = "npm" ] || { [ -z "$PACKAGE_MANAGER" ] && [ -f package-lock.json ]; }; then \
|
|
||||||
npm prune --production; \
|
|
||||||
else \
|
|
||||||
pnpm prune --prod; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Runtime stage ---
|
# --- Runtime stage ---
|
||||||
FROM node:26.8.1-bookworm-slim AS runner
|
FROM node:26.8.1-bookworm-slim AS runner
|
||||||
|
|||||||
@@ -79,13 +79,15 @@ describe("deploy job", () => {
|
|||||||
expect(deployJob).toContain('"database":true');
|
expect(deployJob).toContain('"database":true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prunes old images, containers, volumes, networks and build cache", () => {
|
it("preserves recent build cache and avoids pruning application volumes", () => {
|
||||||
expect(deployJob).toContain("docker image prune -af");
|
expect(deployJob).toContain(
|
||||||
expect(deployJob).toContain("docker container prune -f");
|
'docker builder prune -af --filter "until=72h" --keep-storage=2g',
|
||||||
expect(deployJob).toContain("docker volume prune -f");
|
);
|
||||||
expect(deployJob).toContain("docker network prune -f");
|
expect(deployJob).toContain('docker image prune -f --filter "until=168h"');
|
||||||
expect(deployJob).toContain("docker builder prune");
|
expect(deployJob).not.toContain("docker volume prune");
|
||||||
expect(deployJob).toContain("Prune Docker cache");
|
expect(deployJob).not.toContain("docker network prune");
|
||||||
|
expect(deployJob).not.toContain("docker container prune");
|
||||||
|
expect(deployJob).not.toContain("docker image prune -af");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not run pnpm test in deploy", () => {
|
it("does not run pnpm test in deploy", () => {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const dockerfile = readFileSync("Dockerfile", "utf8");
|
||||||
|
|
||||||
|
describe("Docker build cache", () => {
|
||||||
|
it("installs frozen dependencies before copying application source", () => {
|
||||||
|
const manifests = dockerfile.indexOf(
|
||||||
|
"COPY package.json *lock* pnpm-workspace.yaml .npmrc ./",
|
||||||
|
);
|
||||||
|
const install = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||||
|
const source = dockerfile.indexOf("COPY . .");
|
||||||
|
expect(manifests).toBeGreaterThan(-1);
|
||||||
|
expect(install).toBeGreaterThan(manifests);
|
||||||
|
expect(source).toBeGreaterThan(install);
|
||||||
|
});
|
||||||
|
it("retains the package store and Next compiler cache across source changes", () => {
|
||||||
|
expect(dockerfile).toContain("id=epicnext-pnpm,target=/pnpm/store");
|
||||||
|
expect(dockerfile).toContain("--store-dir=/pnpm/store");
|
||||||
|
expect(dockerfile).toContain("id=epicnext-next,target=/app/.next/cache");
|
||||||
|
});
|
||||||
|
it("ships standalone output without a redundant dependency pruning step", () => {
|
||||||
|
expect(dockerfile).toContain("/app/.next/standalone ./");
|
||||||
|
expect(dockerfile).not.toContain("pnpm prune --prod");
|
||||||
|
expect(dockerfile).not.toContain("npm prune --production");
|
||||||
|
expect(dockerfile).not.toContain("yarn install --production");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user