July 16, 2026 · 2 min read

Two things the Dockerfile had to get right

Share

Getting this shipped to Coolify meant a Dockerfile, and two things in it turned out to be less obvious than they looked.

--prod doesn't mean what it sounds like

The build stage installs everything — devDependencies included — compiles the app, then the runtime stage only needs the production deps. The obvious move is a fresh pnpm install --prod there. That doesn't actually shrink anything here: the icon packages (@lucide/svelte, @icons-pack/svelte-simple-icons) declare svelte as a peerDependency, and pnpm's autoInstallPeers resolves that whole peer chain — svelte@sveltejs/kitviteesbuild/rollup/lightningcss — regardless of --prod, since peerDependencies aren't classified as dev dependencies. The entire dev toolchain comes back anyway, just through a different door.

The fix is to prune the already-resolved tree from the build stage instead of asking pnpm to resolve a fresh one:

pnpm prune --prod alone still leaves the dev toolchain physically on disk, for the same peer-chain reason — so the leftover .pnpm directories get removed explicitly afterward. Confirmed nothing on that list is ever require()'d at runtime by grepping the compiled build/ output: adapter-node's output is self-contained.

No curl for the HEALTHCHECK

Both stages run on node:22-slim, which doesn't ship curl or wget — and installing either just for a HEALTHCHECK isn't worth the extra image layer. Node itself can make the request instead:

Dropped straight into the Dockerfile as the HEALTHCHECK CMD, polling /healthz — a route that already existed for exactly this — so Coolify (and docker ps) can see whether the container is actually serving, not just that the process is alive. --interval, --timeout, and --start-period keep it from flapping while the server boots.