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);