Curing Image Bloat With Scratch and Distroless Base Images
Shrink container images with multi-stage builds, the empty scratch base, and distroless runtimes — smaller pulls, fewer CVEs, faster starts.
Netbay Infrastructure Team
Netbay Engineering
On this page
A container image that ships a full OS plus build tools you will never run is the most common source of cloud waste: slow pulls, a larger registry bill, and a bigger attack surface for the same one-line binary. The cure is picking a base you actually need at runtime. For compiled languages that means the empty scratch image or a distroless runtime, and for interpreted languages it means a minimal runtime without a shell and package manager.
This post shows how to reduce a typical image to a tenth of its size while keeping it fully runnable.
The Multi-Stage Baseline
Before trimming the base, build in stages so the final image never contains compilers. The classic pattern compiles or assembles in one stage and copies only the artifacts and their minimal runtime into a second stage.
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]The Alpine result is already a fraction of the build stage, but you can go further.
The Scratch Base: Nothing but Your Binary
scratch is an empty layer — literally no filesystem. A statically linked binary runs fine on top of it, which is why CGO_ENABLED=0 matters: it stops Go from linking against glibc. The result is often under 20MB with zero package manager and zero shell.
FROM scratch
COPY --from=build /app/server /server
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
USER 65534:65534
ENTRYPOINT ["/server"]Run it with docker run, and docker image ls shows a tiny footprint. The tradeoff is real: no shell means debugging inside the container is harder, so keep a debug build or docker cp in your toolkit.
Distroless: A Minimal Runtime Layer
Distroless images from Google contain just enough to run a language runtime — glibc, certs, and timezone data — with no shell, no package manager, and running as a non-root user by default. They are the middle ground for languages that need a runtime but not an OS.
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/build /app
EXPOSE 3000
CMD ["/app/index.js"]Pull size drops and so does the expected CVE count, because there is no userland to patch beyond the runtime itself.
Measuring the Win
See the difference with docker history and layer inspection, then enforce a size budget in CI:
docker build -t myapp:trim .
docker image ls myapp:trim
docker history myapp:trim --no-trunc | head
docker scout cves myapp:trim # lower exposed packagesTakeaway
Multi-stage builds, the empty scratch base, and distroless runtimes slice image size and attack surface at once, and are the standard for production Go, Rust, Node, and Python services. Slim images deploy faster and are cheaper to pull repeatedly. Put your trimmed image on a Netbay VPS — provision one at netbayhosts.in and time how quickly a sub-20MB image goes from pull to running.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance