diff --git a/deploy/Dockerfile b/deploy/Dockerfile index f98164d..709eaf7 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -54,10 +54,12 @@ FROM toolchain AS build COPY . . RUN --mount=type=cache,id=ekklesia-pnpm-store,target=/pnpm/store \ pnpm install --frozen-lockfile --store-dir /pnpm/store -# The same gates `pnpm verify` runs for modules and migrations, against exactly the module set in this -# context. Each script builds first; the final build is a Turbo cache hit that keeps the intent -# explicit. -RUN pnpm modules:validate && pnpm migrations:validate && pnpm build +# The same gates `pnpm verify` runs for boundaries, modules, and migrations, against exactly the +# module set in this context. `pnpm boundaries` comes first and reads only sources: among its rules, +# a module that imports something at run time without declaring it a production dependency fails +# here, because the production-only install below would leave it unresolvable at API startup. Each +# validate script builds first; the final build is a Turbo cache hit that keeps the intent explicit. +RUN pnpm boundaries && pnpm modules:validate && pnpm migrations:validate && pnpm build # What the runtime resolves at startup, at the paths it resolves them from: # - every workspace package's `dist/` (every package exports only from `dist/`), which includes # the Dashboard and display host assets under `surfaces/shell/dist/browser/`; diff --git a/deploy/README.md b/deploy/README.md index 401f0bf..4a4066e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -27,7 +27,7 @@ this file. | `base` | `node:24.18.0-trixie-slim`, pinned by its multi-architecture index digest. Debian, so glibc, because module dependencies such as sharp ship glibc prebuilt binaries. Every stage starts here. | | `toolchain` | Enables pnpm through Corepack, at exactly the root `packageManager` version. | | `manifests` | Collects the lockfile, workspace configuration, `.npmrc`, and every workspace `package.json` at its repository path. | -| `build` | `pnpm install --frozen-lockfile`, then `pnpm modules:validate`, `pnpm migrations:validate`, and `pnpm build`, against exactly the modules in the context. Never uses host `node_modules`: `.dockerignore` excludes them. | +| `build` | `pnpm install --frozen-lockfile`, then `pnpm boundaries`, `pnpm modules:validate`, `pnpm migrations:validate`, and `pnpm build`, against exactly the modules in the context. Never uses host `node_modules`: `.dockerignore` excludes them. | | `prod-deps` | The same lockfile with `--prod`: production dependencies only. | | `runtime` | Production dependencies plus built outputs, owned by root and run as the unprivileged `node` user (uid 1000). No pnpm, no compiler, no curl. | @@ -62,6 +62,14 @@ set `DOCKER_BUILDKIT=1` if your Docker CLI has no buildx plugin. No secret is a build argument, and none enters any layer. `.env` files are excluded from the build context entirely, and no registry credentials are needed. +**A module missing a production dependency fails the build, not the API.** The runtime receives +production dependencies only, so a module that imports a package at run time but declares it only +as a development or peer dependency would build and then fail at startup. `pnpm boundaries` runs +in the build stage and rejects that module first +(`runtime-imports-are-production-dependencies`, see +[Repository boundaries](../docs/architecture/repository-boundaries.md)). The same applies to every +other boundary rule: a module the platform's own CI would reject cannot be built into an image. + **A platform-only image does not exercise native module dependencies.** With no modules in the context, nothing like sharp is installed, so the CI image build proves nothing about them. An instance build that includes such a module must smoke-test the built image itself: start it,