Docker

Docker Multi Stage Builds vs Distroless Images for Go Services

4 min read by DebuggedIt

Quick answer

Multi-stage builds and distroless images are not competing choices. A multi-stage build is a Dockerfile technique that compiles your Go binary in one stage and...

Multi-stage builds and distroless images are not competing choices. A multi-stage build is a Dockerfile technique that compiles your Go binary in one stage and copies only the result into a final stage, while a distroless image is one possible base for that final stage. Most production Go services should use both together.

What Is the Difference Between Multi-Stage Builds and Distroless Images?

A multi-stage build separates build-time tooling from runtime. The first stage uses the full Go toolchain image to download modules and compile. The second stage starts from a minimal base and receives only the compiled binary through COPY --from. The Go compiler, source code, and module cache never reach production.

Distroless is a family of minimal base images that contain your application's runtime dependencies, such as CA certificates and timezone data, but no package manager, no shell, and no common Unix utilities. That shrinks the attack surface and the number of packages a vulnerability scanner can flag.

FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
ENTRYPOINT ["/api"]

Pin the Go version to the one declared in your go.mod, and check the distroless project documentation for current image tags, since they change over time.

When Should a Go Service Use Distroless, Scratch, or Alpine?

The deciding factor is whether your binary is statically linked. With CGO_ENABLED=0, Go produces a static binary that needs no libc, which makes both scratch and distroless static viable. If you rely on cgo, for example for certain database drivers or specific DNS resolution behavior, you need a base with a compatible C library.

  • Scratch: the smallest possible image, but it has no CA certificates, no timezone data, and no non-root user unless you add them yourself.
  • Distroless static: includes CA certificates, timezone data, and a nonroot user, so outbound HTTPS works out of the box.
  • Distroless base: adds glibc for cgo binaries.
  • Alpine: includes a shell and package manager, which helps debugging but increases the attack surface. It uses musl, which can behave differently from glibc with cgo.

For most Gin APIs that call external services over HTTPS, distroless static is the safest default, because missing CA certificates in scratch images is a common cause of TLS errors at runtime.

What Are the Debugging Trade-Offs of Distroless Images?

Because distroless has no shell, you cannot docker exec into a container and poke around. That is deliberate, but it changes your workflow. You need to rely on structured logs, metrics, and health endpoints instead of interactive inspection.

When you do need a shell, distroless publishes debug variants that include BusyBox. Use them in staging only, and keep production on the standard tag. Kubernetes ephemeral debug containers are another option, though availability depends on your cluster version, so confirm in the Kubernetes documentation.

Best Practices for Go Container Images

  • Copy go.mod and go.sum before the source so dependency layers are cached between builds.
  • Run as a non-root user, using the nonroot tag or an explicit USER instruction.
  • Build with -trimpath and consider -ldflags="-s -w" to reduce binary size, knowing it removes debug symbols.
  • Scan the final image in CI and rebuild regularly to pick up base image patches.
  • Pin base images by digest for reproducible releases.

Frequently Asked Questions

Is distroless smaller than Alpine?

Distroless static images are usually a few megabytes, comparable to or smaller than Alpine. Scratch is smaller still. Image size matters less than the reduced package count, which lowers vulnerability noise.

Can I use distroless with a Go binary that needs cgo?

Yes, but use the distroless base image that ships glibc instead of the static variant, and build in a stage whose C library matches. Verify the linkage with ldd on the binary.

Why does my scratch-based Go container fail on HTTPS calls?

Scratch has no CA certificate bundle, so TLS verification fails. Copy certificates from the build stage or use distroless static, which already includes them.

Do I still need multi-stage builds if I use distroless?

Yes. Distroless contains no compiler, so you must build the binary elsewhere. A multi-stage build is how the binary gets into the distroless image.

Summary

Use a multi-stage build to compile and a distroless static image to run your Go service. Choose scratch only if you will manage certificates yourself, and use the debug variant or ephemeral containers when you need a shell. Always check the official documentation for current image tags and version-specific behavior.