THIS EXPLANATION
THE ROOM
CDA·23 Computing, Data & AI 6 MIN · 8 STATIONS

Building somewhere you do not ship from

A Socratic walk-through of building somewhere you do not ship from — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why assemble software in one environment and then throw that environment away?

The straightforward way to package a program is to put everything it needed into one place: the compiler, the headers, the package manager, the source, the test framework, and the finished binary. It all worked together, so ship it all together.

The result is an image of eight hundred megabytes to run a twelve-megabyte binary. And the reflex is to call that waste and start deleting. But hold the question open a moment longer, because there is something more interesting here than size: is the compiler in the shipped image merely surplus, or is it actively a problem? The answer decides whether this is an optimisation or a design principle.

b

Reasoning it through

REASONING #

Start with the surplus reading and see how far it gets. A larger image is slower to pull, so deployments and autoscaling are slower, and every node stores more. Real costs, but they are only costs — you would accept them if the toolchain were doing anything useful.

So ask whether it is. Once the binary exists, does the compiler ever run again? It does not. Nor does the package manager, nor the headers, nor the test framework. Every one of them was needed to produce the artifact and none is needed to execute it. Two entirely different jobs, and we assumed they wanted the same environment only because they happened in the same directory.

Now push on what the surplus actually does. Every program in the image is something an attacker who gains execution can use. A compiler lets them build tools in place; a package manager lets them fetch more; a shell and a full set of utilities let them explore. None of that is available in an image containing a binary, its runtime libraries, and nothing else. This is why the size argument, though true, undersells the case — the toolchain is not dead weight, it is capability sitting inside the trust boundary for no reason.

And there is a third cost. The build needed credentials — a token for a private package repository, an SSH key for a private source repository. Ship the build environment and those may still be in it. Recall that image layers are difference archives: deleting a secret in a later instruction only masks it, and anyone who pulls the image can unpack the earlier layer and read it. So a secret that touched a shipped layer is disclosed, whatever the final filesystem looks like.

That last point is decisive, because it rules out the tempting fix. You cannot solve this by cleaning up at the end. Removal is not available to you — an earlier layer cannot be edited. The toolchain has to never enter the chain that ships.

So how? Do the build in a separate stage: a first stage on a fat image with everything the compiler needs, and a second stage that starts from a minimal base and copies in only the produced artifact. The second stage's layer chain has no ancestor in common with the first, so nothing from the build environment travels — not the source, not the toolchain, not the credentials. The first stage still runs, still gets cached, and simply is not published.

Notice how far the second stage can be pared down. A statically linked binary needs almost nothing, so the base can be scratch — literally empty — or a distroless image that carries a runtime and certificate bundle but no shell and no package manager. A dynamically linked program needs its shared libraries and often a certificate store, so scratch will fail at startup with an error that looks mysterious if you have not met it before. And an interpreted language cannot shed its interpreter at all, so the gain there is narrower: you drop the build toolchain and the development headers, but the runtime stays.

That last point is worth keeping honest. The technique is not universal magic. Its size benefit is largest for compiled languages and modest for interpreted ones — but the security benefit, the removal of a shell and a package manager from the shipped image, applies to both.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a shipwright's yard and the ship that leaves it. The yard has cranes, scaffolding, moulds, saws, and stacks of unused timber, all of it essential to producing the hull. None of it goes to sea. Nobody regards leaving the scaffolding behind as an economy measure — the scaffolding is simply not part of the ship, and a vessel that sailed with its cranes still bolted on would be worse in every respect, not merely heavier.

WHERE IT BREAKS DOWN

a ship physically cannot carry its own dry dock, so the separation enforces itself, whereas a container image will happily carry its entire build environment and run perfectly well — which is exactly why the mistake is so easy to make and so easy to leave in place for years.

d

Clarifying the model

THE MODEL #

Three refinements.

First, the misconception: multi-stage builds are usually introduced as a size optimisation, which sets the wrong expectation. Size is the most visible consequence. The design point is that building and running are different activities with different dependencies, and giving each its own environment is the ordinary answer once you have noticed that.

Second, the stages are genuinely independent. The build stage can use a completely different base, a different distribution, even a different language ecosystem, because the only contract between the stages is the file that gets copied forward. That freedom is what lets the build stage be as convenient as you like without any of it reaching production.

Third, minimal does not mean easy. An image with no shell cannot be inspected by opening a shell in it, so debugging moves to other techniques — copying the binary into a fuller image, or attaching an ephemeral debug container. That is a real trade, and it is worth making deliberately rather than discovering it during an incident.

e

A picture of it

THE PICTURE #
Building somewhere you do not ship from
Building somewhere you do not ship from Read left to right, and read the band widths as quantities of shipped bytes -- the figures are illustrative of a typical compiled service, not measurements. Two large inputs flow into the build stage, and almost all of that volume leaves by the "discarded" band, which goes nowhere: that band is the whole point of the diagram, since it never reaches the right-hand side and so never reaches a registry or a running host. Only the thin "binary" band continues, joining a small runtime base to form the final image. Compare the total width entering the build stage with the width arriving at the final image to see what the separation buys. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/building-somewhere-you-do-not-ship-from.md","sourceIndex":1,"sourceLine":4,"sourceHash":"9bcd8884764873ddcc2d4448a9d2497e1ae4e460b2891c07d76cdc8b88df4743","diagramType":"sankey","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":545},"qa":{"passed":true,"findings":[]}} Sourcetree · 180 Buildstage · 820 Toolchain · 640 Binary · 40 Discarded · 780 Finalimage · 65 Runtimebase · 25

How to readRead left to right, and read the band widths as quantities of shipped bytes — the figures are illustrative of a typical compiled service, not measurements. Two large inputs flow into the build stage, and almost all of that volume leaves by the "discarded" band, which goes nowhere: that band is the whole point of the diagram, since it never reaches the right-hand side and so never reaches a registry or a running host. Only the thin "binary" band continues, joining a small runtime base to form the final image. Compare the total width entering the build stage with the width arriving at the final image to see what the separation buys.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Building and running want opposite environments — one rich in tools, one stripped of them — and because image layers can only be added to, the toolchain has to be kept out of the shipped chain rather than removed from it afterwards. Separating the stages is therefore not a cleanup step but the only way to get an image that never contained the compiler, the source, or the credentials in the first place.

g

Where to go next

ONWARD #
  • Distroless and scratch bases, and how to tell which one a given binary can actually use.
  • Build secrets mounted for a single instruction so they never enter a layer.
  • Debugging an image with no shell: ephemeral containers and copy-out techniques.
h

Key terms

TERMS #
TermWhat it means
Multi-stage builda build file with several independent stages, where the final stage copies only chosen artifacts from earlier ones.
Build stagean intermediate stage that runs the compiler and package manager and is not published.
Distrolessa minimal base image containing a language runtime and certificates but no shell or package manager.
scratchthe empty base image, usable when a binary is statically linked and needs nothing else.
Attack surfacethe set of capabilities available to someone who gains execution inside the image.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4