Day 126: The Build Tools Don't Belong in Production
On Day 122 I split the Pinchy image into build and runtime stages and it shrank enough to fix a deploy timeout. Today the OpenClaw runtime image got the same treatment, and the before picture is a little embarrassing. The single-stage image was shipping a full C build toolchain to production: build-essential, python3, and on top of that a roughly 300 MB npm download cache. None of it does anything once the gateway is running.
Why a compiler was riding along
OpenClaw’s plugins have native modules, and native modules have to be compiled, which needs Python and a C toolchain at build time. The lazy way to get that is to install the toolchain in the image, build, and leave it there. It works, and it quietly bloats every deployment with hundreds of megabytes of tools that mattered only for the few seconds of compiling. A multi-stage build draws the line properly: a builder stage installs the toolchain, does the compiling, and produces the artifacts; a clean node:22-slim runtime stage copies only those finished artifacts and installs only the three small packages the gateway actually uses at boot, git, inotify-tools, and procps. The compiler never reaches production. The image came down 38.6% (PR #567).
The symlink that broke the launcher
There was one trap on the way worth writing down. An early version copied the openclaw launcher across stages with COPY --from=builder /usr/local/bin/openclaw. That looks right and is quietly broken: COPY --from follows symlinks and copies the target’s contents, and the launcher is a symlink. So instead of a working launcher you get a flat file sitting in the wrong place, and the loader that expects to resolve its real build output next to the original symlink points at a path that does not exist. The fix was to copy the whole install tree with the symlink intact, rather than the dereferenced launcher. It is the kind of bug that passes every glance and fails the moment you actually run it.
Smaller is a security property
The faster deploy is the obvious win, and it is the one that fixed the intermittent one-click 504s, where the old image simply took too long to come up. But the reason I keep doing this work is not speed. A self-hosted Pinchy is something a customer’s security team has to vet, and every megabyte of toolchain in the running image is more for them to account for and more for an attacker to find lying around. A production image with no compiler and no package manager in it is not just lighter. It is a smaller promise to keep. Shrinking it is governance work wearing a perf hat.
While the image was open, so was the logo
In the same stretch I finally fixed something cosmetic that had been bugging me: the mascot was a heavy raster image. It is now a clean, optimized vector, about 8 KB of SVG, scalable to any size without blurring, used across the documentation, the eight places it appears in the app, and the chat. A small thing, but a self-hosted product is judged on whether the details are sharp, and a fuzzy logo is the kind of detail that quietly says nobody is minding the shop.
Day 126
Two unglamorous jobs, one theme. The work no user asks for, taking the compiler out of the shipping image, making the logo crisp, is the work that decides whether the thing feels trustworthy when someone finally looks closely. Especially for a product you run on your own hardware, where the buyer is going to look closely on purpose. Most of building in public is the war stories. But a fair amount of it is just keeping the house in order, on the days when keeping the house in order is the whole job.