There is a tension in building images. To build software you need compilers, package managers, and development headers. To run it you need almost none of that - just the finished program and a runtime. If you build and run in the same image, all those build tools ride along into production, bloating the image for no benefit.
Picture a compiled app - Go, Rust, C, or a front-end bundle. Building it needs the full toolchain:
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
The golang:1.22 image is around 800 MB because it carries the entire
Go toolchain. But the thing you actually run is a single compiled
binary that might be 15 MB. The final image is fifty times bigger than
it needs to be, and every one of those extra megabytes is compiler
tooling that just sits in production doing nothing.
A fat image is not only untidy:
A production image should contain what it needs to run and nothing more.
The answer is to build in one environment and run in another, cleaner one, then move only the finished artifact between them. Docker builds this into a single Dockerfile with multi-stage builds, which is the rest of this topic. The result: a full toolchain does the build, and a tiny image ships to production carrying only the binary.