July 16, 2026 · 2 min read
Two things the Dockerfile had to get right
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/kit → vite → esbuild/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.