From 36ece4af25317b3a0e17125a5882097f6a667d19 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Mon, 14 Sep 2026 03:19:03 +0200 Subject: [PATCH] chore(docker): shrink runtime image and stop building arm64 under QEMU - Pin the toolchain used by the deps and build stages to --platform=$BUILDPLATFORM. The Nitro bundle is pure JavaScript and identical across target platforms, so those stages now run once, natively, instead of once per target (arm64 was running the full pnpm install and Vite build under QEMU). The flag is applied to the image reference because BuildKit ignores it on a stage reference. - Replace the full `pnpm install --prod` in the prod-deps stage with a minimal manifest listing only the packages imported outside Nitro's bundle (instrument.server.mjs and scripts/*.mjs): @sentry/tanstackstart-react, drizzle-orm, and pg. Nitro traces and inlines everything else, so the previous production install (~200 packages, ~210MB) was mostly dead weight. Final image goes from ~549MB to ~108MB. The manifest is generated in the build stage from the installed versions (docker/write-runtime-package.mjs) so the Sentry SDK loaded via --import can never drift from the one bundled into .output. - Add a pull_request trigger (build-only, no push) to docker-publish.yml so a broken Dockerfile is caught before merging to main. The existing `if: github.event_name != 'pull_request'` guards were already written for this but the trigger was missing. - Add a concurrency group so overlapping pushes to main don't run redundant builds. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/docker-publish.yml | 6 ++++ Dockerfile | 42 +++++++++++++++++++++------- docker/write-runtime-package.mjs | 24 ++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 docker/write-runtime-package.mjs diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f8ef998..8beac02 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -4,10 +4,16 @@ on: push: branches: [main] tags: ["v*.*.*"] + pull_request: + branches: [main] env: REGISTRY: ghcr.io +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index d62f65a..cffb908 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,58 @@ # syntax=docker/dockerfile:1 ARG NODE_IMAGE=node:22-bookworm-slim +ARG PNPM_VERSION=12.3.4 -FROM ${NODE_IMAGE} AS base +# Toolchain for the stages that produce platform-independent output. Pinned to +# the build host's platform so that, in a multi-platform build, dependency +# installation and the Vite/Nitro build run once natively instead of once per +# target platform (the arm64 target would otherwise run them under QEMU). +# Note: --platform must be on the image reference; BuildKit ignores it when +# applied to a FROM that points at another stage. +FROM --platform=$BUILDPLATFORM ${NODE_IMAGE} AS build-base +ARG PNPM_VERSION ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -RUN corepack enable && corepack prepare pnpm@12.3.4 --activate +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate # Install dependencies (including dev deps, needed to build) with pnpm's # content-addressable store cached across builds. -FROM base AS deps +FROM build-base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ pnpm config set store-dir /pnpm/store \ && pnpm install --frozen-lockfile -FROM base AS build +# Build the Nitro server output and derive the runtime manifest from the +# installed (lockfile-resolved) versions; see docker/write-runtime-package.mjs. +FROM build-base AS build WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN pnpm run build +RUN pnpm run build && node docker/write-runtime-package.mjs + +# Toolchain for the target platform, used to install the runtime node_modules +# for the architecture the image will actually run on. +FROM ${NODE_IMAGE} AS target-base +ARG PNPM_VERSION +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate # The instrumentation and migration bootstrap run outside Nitro's bundle, so -# their imports must be resolvable from production node_modules. -FROM base AS prod-deps +# their imports must be resolvable from production node_modules. Nitro traces +# and inlines everything else into .output/server, so only the packages those +# entry points import directly are installed here, instead of the full +# production dependency tree. Lifecycle scripts are skipped because the only +# one in this tree (@sentry/cli's binary download) is build-time tooling. +FROM target-base AS prod-deps WORKDIR /app -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY --from=build /app/runtime-package.json ./package.json RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ pnpm config set store-dir /pnpm/store \ - && pnpm install --frozen-lockfile --prod + && pnpm install --no-frozen-lockfile --ignore-scripts -# Final runtime image: the built Nitro server output plus production node_modules. +# Final runtime image: the built Nitro server output plus the minimal +# production node_modules above. FROM ${NODE_IMAGE} AS runtime ENV NODE_ENV=production ENV HOST=0.0.0.0 diff --git a/docker/write-runtime-package.mjs b/docker/write-runtime-package.mjs new file mode 100644 index 0000000..293de93 --- /dev/null +++ b/docker/write-runtime-package.mjs @@ -0,0 +1,24 @@ +// Writes a minimal package.json for the runtime image. +// +// Nitro traces and inlines the server's dependencies into .output/server, so +// the only packages that must exist in the runtime image's node_modules are +// the ones imported by the entry points that run outside the bundle: +// instrument.server.mjs and scripts/*.mjs. Versions are read from the +// installed node_modules (i.e. from pnpm-lock.yaml) so they cannot drift from +// the versions bundled into .output, which matters for Sentry in particular. +import { readFileSync, writeFileSync } from "node:fs"; + +const runtimePackages = ["@sentry/tanstackstart-react", "drizzle-orm", "pg"]; + +const dependencies = Object.fromEntries( + runtimePackages.map((name) => { + const { version } = JSON.parse(readFileSync(`node_modules/${name}/package.json`, "utf8")); + return [name, version]; + }), +); + +writeFileSync( + "runtime-package.json", + `${JSON.stringify({ name: "auth-runtime", private: true, type: "module", dependencies }, null, 2)}\n`, +); +console.info("runtime dependencies:", dependencies);