diff --git a/Dockerfile b/Dockerfile index b4f9f75..a9e330b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,32 @@ -# The tool runtime: the base every module written in this toolchain is compiled on top of, and the -# container a node runs to serve them. It is handed the broker credential and the assigned modules' -# entrypoints at deploy time and serves them. +# Two images from one recipe: the one modules are COMPILED in, and the one they RUN in. # -# **Built from this repository alone.** It used to copy in a compiled output directory that is not -# in source control, and resolve the mesh's own toolkit to a sibling checkout on the same disk — so -# it could only be produced on a workstation with two repositories laid out side by side, and its -# fingerprint was then typed into every module's recipe by hand. That put the one artifact the whole -# toolchain stands on outside the toolchain: nothing could rebuild it, so nothing could check it, -# and the rule that catches a base moving had no version on the far end of its edge and could never -# fire (novox/hq issue 044). +# **They were the same image, and that was a mistake.** A module's recipe starts from this and +# invokes the compiler out of it, so the compiler had to be here — and because the same image was +# also what every module ran in, every running container on every machine carried a TypeScript +# compiler it would never invoke. 23 of the 28 MB of libraries were that compiler. It was defended +# in a comment, which made a workaround look like a decision: the earlier attempt to prune the build +# tools produced a smaller image that nothing could be built on, and the answer to that is two +# images rather than one image that is bad at both jobs. # -# Debian rather than Alpine, and root rather than an unprivileged user, because that is what the -# image actually in service is — and modules have already been built against it, one of which -# installs a package with Debian's package manager. This recipe said Alpine while serving Debian for -# as long as nobody could rebuild it to notice. Changing the operating system under every module is -# a separate decision from making this buildable, and is not being taken here. -FROM node:22-bookworm-slim AS build +# Kept in one recipe deliberately. They must agree about the operating system, the language version +# and the library, and two files drift. `build.artifacts` in module.json names a stage each. + +# ---- toolchain: what a module is compiled in ------------------------------------------------- +FROM node:22-bookworm-slim AS toolchain # git, because a dependency named by a git URL is fetched by git and this image does not carry it. -# Only in the build stage: what it is needed for happens here, and a runtime that can clone is a -# runtime that can be made to clone. +# Only here: what it is needed for happens at build time, and an image that can clone is an image +# that can be made to clone. RUN apt-get update \ && apt-get install -y --no-install-recommends git ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package.json package-lock.json ./ -# Development dependencies included: the compiler is one of them, and so is the toolkit's own — it -# builds itself on install, which is what lets it be named by a git URL rather than fetched from a -# package registry this mesh does not yet run. +# Development dependencies included: the compiler is one, and so is the toolkit's own. RUN npm install --no-audit --no-fund -# **And then compile the toolkit, because npm did not.** It declares a `prepare` script, which is -# the hook npm is supposed to run after installing a package from git — and this npm does not run -# it, so the package arrives as sources with every one of its entry points pointing at a compiled -# directory that is not there. The compile is therefore done here, explicitly: install the toolkit's -# own build dependencies inside it, build it, then drop them again so they do not travel into the -# image. Doing it by hand rather than relying on the hook is also the honest arrangement — a build -# that silently depended on a hook firing would break the day it stopped, in the same invisible way. +# **And then compile the toolkit, because npm did not.** It declares a `prepare` script, the hook a +# package manager is supposed to run after installing from git, and this one does not run it — so +# the package arrives as sources with every entry point pointing at a compiled directory that is not +# there. Done explicitly rather than relying on a hook firing, which would break the day it stopped. RUN npm --prefix node_modules/@novox/mesh-sdk install --no-audit --no-fund \ && npm --prefix node_modules/@novox/mesh-sdk run build \ && npm --prefix node_modules/@novox/mesh-sdk prune --omit=dev @@ -42,16 +34,17 @@ COPY tsconfig.json ./ COPY src ./src RUN npm run build -# Everything the modules compile against and run on. -# -# **The build dependencies stay, and that is deliberate.** This image is not only what a module runs -# in — it is also what every module is *compiled* in: a module's recipe starts from this and invokes -# the compiler out of these same directories. Dropping them would halve the image and break every -# module that builds on it, which is the sort of tidy-looking change that only fails somewhere else. -# If the two roles are ever separated, they should be separated deliberately and named separately. -FROM node:22-bookworm-slim +# ---- what the running image needs, and nothing else ------------------------------------------- +# Its own stage so the toolchain image keeps its build tools while the runtime image does not. The +# prune has to happen somewhere, and doing it in the toolchain stage would take the compiler out of +# the image whose whole purpose is to have one. +FROM toolchain AS lean +RUN npm prune --omit=dev + +# ---- runtime: what a module runs in ----------------------------------------------------------- +FROM node:22-bookworm-slim AS runtime WORKDIR /app COPY package.json ./ -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/dist ./dist +COPY --from=lean /app/node_modules ./node_modules +COPY --from=toolchain /app/dist ./dist ENTRYPOINT ["node", "dist/main.js"] diff --git a/module.json b/module.json index d6e0e5d..bb96901 100644 --- a/module.json +++ b/module.json @@ -4,7 +4,18 @@ "slug": "tools", "build": { "artifacts": [ - { "name": "runtime", "kind": "image", "from": "Dockerfile" } + { + "name": "build", + "kind": "image", + "from": "Dockerfile", + "target": "toolchain" + }, + { + "name": "runtime", + "kind": "image", + "from": "Dockerfile", + "target": "runtime" + } ] }, "resources": []