Multi-Stage Docker Builds for Go
Multi-stage Dockerfiles compile Go in a full SDK image, then copy only the binary (and required trust roots) into a minimal runtime stage.
Search across all documentation pages
Multi-stage Dockerfiles compile Go in a full SDK image, then copy only the binary (and required trust roots) into a minimal runtime stage.
The result is small, fast-pulling images with fewer OS packages to patch.
Go's static binaries pair naturally with distroless or scratch runtimes.
A typical pipeline uses a golang build stage, emits CGO_ENABLED=0 output, and copies the executable into gcr.io/distroless/static or a custom scratch image with CA certs.
HTTPS clients need the system certificate bundle unless you embed roots another way.
BuildKit cache mounts for go mod download shrink CI time without bloating layers.
Quick-reference recipe card - copy-paste ready.
# syntax=docker/dockerfile:1
FROM golang:1.26-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath \
-ldflags="-s -w" -o /out/api ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]When to reach for this:
Production-oriented Dockerfile with CA certs for outbound TLS:
# syntax=docker/dockerfile:1
FROM golang:1.26-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
ARG VERSION=dev
ARG COMMIT=none
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
-o /out/api ./cmd/api
FROM debian:bookworm-slim AS certs
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& update-ca-certificates
FROM gcr.io/distroless/base-debian12:nonroot
COPY --from=build /out/api /api
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]docker build \
--build-arg VERSION=1.0.0 \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
-t myorg/api:1.0.0 .What this demonstrates:
go mod download in CI.certs stage copies only ca-certificates.crt into distroless.nonroot user avoids running as UID 0 inside the container.COPY --from= appear in the final image history.CGO_ENABLED=0 avoids glibc dynamic loader requirements on Linux amd64/arm64.| Base | Shell | libc | CA certs | Typical size |
|---|---|---|---|---|
| scratch | No | No | Manual | Smallest |
| distroless/static | No | No | No | ~2 MB + binary |
| distroless/base | No | Yes | Yes | ~20 MB + binary |
| alpine | Yes | musl | Yes | Larger, different libc |
# Verify binary is static before choosing scratch
CGO_ENABLED=0 go build -o api ./cmd/api
file api # expect "statically linked"
ldd api # expect "not a dynamic executable" on LinuxCGO_ENABLED=1 builds; prefer debian bookworm builders for cgo.-trimpath keeps $PWD out of binaries for supply-chain audits.GOAMD64=v3 only when you control CPU baseline and want newer SIMD instructions.RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /out/api ./cmd/apigolang:1.26-bookworm@sha256:....docker scout cves or your scanner on the final stage tag, not the builder.ca-certificates.crt.CGO_ENABLED=0./usr/share/zoneinfo; set TZ or copy zoneinfo if logs need local time.kubectl exec shell in distroless; rely on logs, metrics, and ephemeral debug pods with tooling.CAP_NET_BIND_SERVICE deliberately.Choose scratch when the binary makes no outbound HTTPS and needs the smallest possible image.
Choose distroless/base when you need libc, DNS resolver behavior, and CA certs without maintaining a custom scratch layout.
Run apt-get install ca-certificates in a throwaway debian stage, then COPY --from=certs /etc/ssl/certs/ca-certificates.crt into distroless.
Never run apt in the runtime stage.
You may be tagging the build stage accidentally, or copying test fixtures and build caches.
Inspect with docker history myorg/api:tag and ensure the published tag points to the final stage.
Rarely for cloud microservices.
UPX can trigger antivirus false positives and adds startup decompress cost.
Prefer -ldflags="-s -w" and smaller dependency graphs first.
Listen on 8080 inside the container and let Ingress map port 443/80.
Alternatively use a rootless capability setup, but high ports are the common Kubernetes pattern.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026