Why your container image is 1.4 gigabytes
It should be forty megabytes. Here is where the rest of it came from and how to get it back.
A container image for a compiled service should be tens of megabytes. For an interpreted one, low hundreds. If yours is over a gigabyte, something specific went wrong and it is usually one of six things.
Size matters for real reasons: pull time on cold start, registry cost, deployment speed when you are scaling out under load, and attack surface — every package in the image is something that can have a CVE you have to answer for.
the six causes#
1. You shipped the build toolchain.
The compiler, the headers, the package manager cache, the source tree, the test fixtures. All of it needed to build, none of it needed to run.
Multi-stage builds fix this completely:
FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
ENTRYPOINT ["/app"]Final image: the binary, plus CA certificates and timezone data. Tens of megabytes.
2. You started from a full distribution image.
FROM ubuntu is roughly 80 MB before you install anything, and it includes a package manager, a shell, and a hundred utilities you will never invoke.
The ladder, from largest to smallest:
- Full distribution — 80 MB+
-slimvariants — 30–80 MB- Alpine — 5–10 MB, with musl libc, which will occasionally surprise you
- Distroless — just the runtime, no shell, no package manager
scratch— nothing at all, for static binaries
The Alpine caveat, since it bites people: musl's allocator and DNS resolver behave differently from glibc's. Python performance in particular can be significantly worse, and some binary wheels do not exist for musl. Test rather than assume.
3. Your layers are ordered wrong.
Each instruction creates a layer. A layer is invalidated when it or anything before it changes.
# bad — any source change reinstalls every dependency
COPY . .
RUN npm ci
# good — dependencies are cached until the lockfile changes
COPY package.json package-lock.json ./
RUN npm ci
COPY . .This does not shrink the final image but it dramatically speeds up builds, which is usually what people actually care about.
4. You deleted things in a later layer.
RUN apt-get install -y build-essential # layer 1: +400 MB
RUN apt-get remove -y build-essential # layer 2: marks deleted, image unchangedLayers are additive. Deleting a file in a later layer hides it and does not remove it. The bytes are still in the image and still transferred on pull.
Everything must happen in one RUN:
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& make \
&& apt-get purge -y build-essential \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*Better: use a multi-stage build and do not install the toolchain in the final image at all.
5. You have no .dockerignore.
COPY . . copies .git, node_modules, build artifacts, test fixtures, and your local .env.
.git
node_modules
dist
*.log
.env*
**/__pycache__
coverageThe .git directory alone is frequently hundreds of megabytes on a mature repository, and it is in a lot of images.
6. Your dependencies are enormous.
Sometimes it is genuinely the dependencies — machine learning stacks with CUDA libraries are legitimately multiple gigabytes.
Check whether you need the GPU variant. torch with CUDA is roughly 2.5 GB; the CPU build is a fraction of that. If you are serving on CPU, you are shipping GPU libraries for nothing.
finding out where it went#
docker history --no-trunc <image> # size per layerOr use a layer inspection tool that shows you which files are in which layer and how much space is wasted. Ten minutes with one of those tells you exactly what to fix.
the security dimension#
Every package in the image is potential CVE surface, and your scanner will report all of them regardless of whether the code is reachable.
A distroless image has almost nothing to report, which means the reports you do get are signal rather than noise. That is worth more than the size reduction — a vulnerability report with three entries gets read; one with four hundred does not.
The trade-off: no shell means you cannot docker exec in to debug. Use ephemeral debug containers that attach to the running pod's namespaces instead, which is a better practice anyway because it means your production image is not a debugging toolkit.
the target#
- Compiled language, static binary: under 30 MB.
- Interpreted with dependencies: under 200 MB.
- Anything over a gigabyte without a machine learning stack: something is wrong and it is one of the six above.
— Dom, July 10, 2026