Pakkit.net
← Back to blog

Engineering Practice

Build Where the Tools Are, Run Where the Data Is

The machine where your code has to run is often the worst place to build it — production and appliance boxes shouldn't carry compilers and SDKs — so build the artifact on a tooled machine, ship the whole thing, and run it where the data lives.

  • Engineering Practice
  • Operations
  • Build
  • Security

I needed to run some compiled code directly on a set of database nodes to measure them, and hit a wall that turned out to be a useful one: the nodes had a Java runtime but no compiler. My first instinct was mild annoyance — just install the compiler. My second, better instinct was that the missing compiler wasn’t a gap to fill; it was the machine telling me something correct about where build work belongs. Build where the tools are. Run where the data is. Those are two different places on purpose.

A runtime is not a toolchain, and prod shouldn’t have both

The box that runs your code needs a runtime — the JVM, the interpreter, the shared libraries. It does not need the build toolchain: the compiler, the SDK, the headers, the package-manager dev groups. Those are development-time tools, and a well-kept production or appliance host deliberately doesn’t carry them. That’s not an oversight. A compiler on a production box is extra attack surface, extra to patch, and an open invitation to “quickly” build something in place that then exists nowhere in source control.

So the workflow I settled into: compile on a machine that’s meant for building — my laptop, a CI runner, a dev box — targeting the runtime the node actually has, then copy the resulting artifact over and run it against the runtime and the drivers already installed there. The node stays a clean runtime host. The build stays somewhere it can be reproduced.

The absence of a compiler on a production box isn’t a missing feature. It’s the box keeping its job description honest.

Target the runtime you’ll run on, not the one you build on

The one discipline this demands is matching the build to the target. If the node runs a particular runtime version, you compile for that version, not for whatever’s newest on your laptop. Most toolchains have a flag for exactly this — build against an older target so the artifact runs on the older runtime in the field. Skip that and you get the maddening “compiles fine here, won’t load there” failure, which is just the runtime being an unnamed dependency biting from the other direction. Build local, but build for there.

Ship the whole artifact, not just the obvious part

The other trap is shipping an incomplete artifact. The first time I did this, I copied over the main compiled class, ran it, and got a runtime error about a missing class I’d never heard of — until I realized my code had a helper (an inner class) that compiled into its own separate file, and I’d only shipped one of them. The build produced several pieces; I shipped one; it failed the moment it reached for the piece I left behind.

That’s the quiet hazard of the build-here-run-there split: a partial transfer fails at runtime, on the target, not at build time where you’d notice. So the artifact you ship has to be complete — every class file, every dependency the target doesn’t already provide, every resource. This is exactly why packaging formats exist (a jar, a wheel, a container image, a single static binary): they bundle “all the pieces” into one thing you can’t half-ship. If you’re hand-copying loose files, you’re one forgotten piece away from a confusing failure.

The general shape: separate the build environment from the run environment

Step back and this is just the principle behind every serious deployment pipeline:

  • The build environment has the compilers, SDKs, and dev dependencies. It’s allowed to be messy and powerful because nothing production-facing runs there.
  • The run environment has only the runtime and what the app genuinely needs at execution time. It’s kept minimal because minimal is easier to secure, patch, and reason about.
  • A complete, versioned artifact is the clean handoff between them — built once, shipped whole, run anywhere with the matching runtime.

Cross-compiling for embedded targets, building a container on a beefy runner to run on a tiny node, producing a static binary on CI to drop onto a locked-down host — they’re all the same move. The place with the tools and the place with the data are different, and the artifact is the bridge.

So when a target box is missing a compiler, I’ve stopped reading it as a problem to fix. It’s a boundary to respect: build somewhere built for building, ship a complete artifact, and let the runtime host stay a runtime host. If you’ve got a build-here-run-there pipeline with a sharp edge you smoothed out, I’d like to hear about it.