A 1.2 GB Docker image for running a small Go service might just look like wasted storage, but the real problem shows up when Kubernetes needs to create new pods under pressure. Every node that doesn’t have the image cached has to pull it before the container can start. Separating the build environment from the runtime environment with a multi-stage build can radically cut that weight, although the specific jump from 1.2 GB to 8 MB depends on the application and shouldn’t be read as a universal figure.
Small Docker images in 30 seconds
- Multi-stage builds let you compile an application in a full image and copy only the necessary output to production.
- Docker recommends this technique to reduce size and attack surface.
scratchis a completely empty image and works well for static binaries.- An official
golang:1.22image is around 285 MB compressed in some Linux variants, even before adding any code or custom layers. - Fewer bytes can translate into faster deployments and scaling, especially when the node doesn’t yet have the layers cached.
The example is simple. A Go application can be built perfectly well with a Dockerfile like this:
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
It works.
The problem is that the image used to build the app ends up being the same image used to run the service.
That means shipping to production tools that may have been needed during docker build, but that no longer do anything once the binary is running.
Docker’s own documentation describes exactly this problem: in a traditional build, all instructions run inside the same environment, and the layers needed to download dependencies, compile, and package can end up as part of the final image. Docker recommends multi-stage builds to separate the two worlds.
The compiler doesn’t need to travel to production
The core difference is treating build and runtime as two independent phases.
A Dockerfile for a simple Go application could look like this:
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
COPY --from=build /app/server /server
CMD ["/server"]
The first stage can be as heavy as it needs to be.
It contains Go, the necessary libraries, build tools, and any other dependency used to produce the binary.
Once that stage finishes, only /app/server gets copied to the second image.
The compiler stays behind.
The package manager stays behind.
The tools used during the build stay behind too.
Docker uses essentially this same pattern in its own documentation for Go: a first stage based on golang, followed by another based on scratch, to which only the compiled executable is copied.
scratch also has a distinctive feature: it contains practically nothing, because it represents an empty base image.
It doesn’t include a conventional Linux distribution, a shell, a package manager, or the usual userspace tools.
For a static binary that doesn’t need any of those dependencies, it can make for an extraordinarily small base. Docker specifically recommends scratch as an option for fully static binaries.
That’s where the big reductions can come from.
But one common example circulating on social media deserves a caveat: going from 1.2 GB to 8 MB is entirely possible for a specific application, but it doesn’t mean that golang:1.22 alone weighs 1.2 GB.
Docker Hub lists golang:1.22.12-bookworm at around 284.79 MB compressed on linux/amd64, while the Alpine variant comes in at around 69.91 MB. An application’s additional layers, dependencies, artifacts, caches, and other content can push the final result up considerably.
The right comparison would therefore be:
| Design | What reaches production | Possible size |
|---|---|---|
| Single stage | Runtime + compiler + tools + application | Hundreds of MB or more |
| Multi-stage + minimal image | Minimal runtime + application | Tens of MB |
Multi-stage + scratch | Mainly the binary and explicitly copied files | Can drop to a few MB |
The exact figures depend on the compiled program, architecture, symbols, libraries, and additional files.
Size matters when Kubernetes needs to react quickly
A large image doesn’t necessarily mean a slow startup in every deployment.
Docker and container runtimes work with layers and caches. If a node already has the necessary layers, it doesn’t need to download them again.
The situation changes when a new node shows up.
That can happen during autoscaling, after replacing a machine, when moving workloads between zones, or when Kubernetes schedules the pod on a host that doesn’t yet have that version of the image.
Before the container can start, the necessary layers have to be pulled from the registry.
That’s why image size affects what’s known as the container’s cold start, even though it isn’t the only factor.
A 1.2 GB image and an 8 MB image represent a 150-fold difference in raw volume. The actual time difference will depend on bandwidth, registry latency, previously cached layers, disk speed, and available parallelism.
As a purely mathematical exercise, transferring 1.2 GB over an effective 100 Mbit/s link would take around 96 seconds if you ignored protocols, compression, and any other bottleneck. Downloading 8 MB under those same theoretical conditions would take less than a second.
In production the actual numbers will differ, but the ratio explains why this optimization can matter during a traffic spike.
Kubernetes’ Horizontal Pod Autoscaler can decide it needs more replicas quickly. If those replicas need tens of extra seconds just to pull the image, the new capacity may come online once most of the spike has already passed.
The same difference affects a deployment spread across dozens or hundreds of nodes.
It’s not just space used in a registry — a cost dimension that tools like OVH Cost Manager are increasingly built to track.
It’s traffic.
It’s deployment time.
And it’s recovery from failures too.
Less content also shrinks the attack surface, but scratch comes with trade-offs
There’s another important benefit: any software that isn’t included in an image stops being part of its attack surface.
If the final container doesn’t need a compiler, there’s not much reason to install one.
The same goes for curl, wget, a package manager, or a full collection of system utilities.
Docker explicitly notes that separating the build environment from the runtime through multi-stage builds can reduce both image size and attack surface.
However, turning scratch into a universal recommendation would be another mistake.
An empty image also leaves out elements that some applications need.
You may need to add certificate authority (CA) certificates for HTTPS connections, timezone data, or certain system files. An application that uses CGO may also depend on shared libraries and may not work simply by copying its executable into scratch.
Diagnostic tools disappear too.
Inside a scratch container, you normally won’t find:
sh
bash
curl
ps
cat
ls
Trying to run:
docker exec -it my-container /bin/sh
will not work if /bin/sh simply doesn’t exist.
From a security standpoint, that can be an advantage. From an operations standpoint, it forces you to use other debugging techniques, ephemeral containers, or images built specifically for diagnostics.
That’s why a minimal image doesn’t always have to be an empty image.
Depending on the application, it may make more sense to use a slim base that includes the necessary dependencies, while still keeping the multi-stage build to strip out the entire build toolchain.
It also helps to review .dockerignore. Docker recommends excluding things like .git, local artifacts, or dependency directories that don’t need to be transferred during the build.
The useful question to ask when reviewing a Dockerfile is fairly simple:
is this file or package needed to build the application, or to run it?
If it only belongs to the first group, it normally shouldn’t ship in the production image.
A small image doesn’t automatically make an application fast, secure, or efficient. There can be a 20 MB service with a two-minute startup and another one several hundred megabytes in size that responds instantly.
But cutting hundreds of megabytes that are never used is one of those optimizations that pays off in several places at once: it reduces transfer, storage, distribution time, and the amount of software present inside the container.
For many compiled services, the change starts with something as simple as a second FROM.
Frequently asked questions
What is a multi-stage build in Docker?
It’s a Dockerfile that uses multiple FROM instructions to separate different build phases. It lets you compile the application in one stage and copy only the necessary files into the final image.
Can a Docker image really go from 1.2 GB to 8 MB?
Yes, it’s possible for certain programs, especially small static binaries, but it’s not a guaranteed reduction. The final size depends on the application, its dependencies, and the files it needs at runtime.
Is it advisable to use FROM scratch in production?
It can be, for fully static executables that don’t need additional system components. Other applications require certificates, libraries, timezones, or tools that call for a more complete base image.
Does a smaller Docker image make Kubernetes scale faster?
It can, when nodes need to download the image before creating new pods. If the layers are already stored locally, the difference can be much smaller.

