A Dockerfile builder for Go that turns a handful of choices — Go version, module path, build target, and runtime base — into a clean multi-stage Dockerfile that ships the smallest practical image. It is aimed at developers who want a correct, cache-friendly starting point without re-deriving the static-binary build flags every time.
How it works
A Go container is built in two stages. The builder stage starts from the official
golang image, copies go.mod and go.sum first so the module download is cached as its
own layer, then copies the source and compiles a binary with CGO_ENABLED=0 and
-ldflags="-s -w". With CGO disabled the result is a fully static binary that depends on
nothing at runtime.
The runtime stage then copies just that binary into a minimal base image. scratch is
an empty image — the binary plus a copy of the CA certificate bundle is all that ships.
distroless/static adds a non-root user and is a good default when you want something
slightly more debuggable than scratch. alpine includes a shell and apk, which you
need when CGO is enabled because the binary then links against musl libc.
What the generated Dockerfile looks like
For a module github.com/example/myapp building the ./cmd/server package:
FROM golang:1.22 AS builder
WORKDIR /app
# Cache the module download as its own layer
COPY go.mod go.sum ./
RUN go mod download
# Copy source and compile a fully static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o server \
./cmd/server
# --- runtime stage ---
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
The module download and compilation steps are each in separate RUN layers.
Changing application source without touching go.mod/go.sum reuses the
cached download layer, so most rebuilds skip the module fetch entirely.
Runtime base comparison
| Base | Shell | Non-root user | Size delta | Best for |
|---|---|---|---|---|
scratch | No | No | 0 MB | Fully static binaries |
distroless/static | No | Yes (nonroot) | ~1 MB | Preferred static default |
distroless/base | No | Yes (nonroot) | ~20 MB | CGO binaries (glibc) |
alpine | Yes (sh) | No (add manually) | ~8 MB | Debugging, CGO/musl |
The CA certificates COPY in the scratch example is necessary for any binary that makes outbound HTTPS calls. Without it, TLS verification fails because there is no trust store in an empty image.
Tips and notes
- If you enable CGO, do not use
scratch— the binary links against libc at runtime and will fail to start with “no such file or directory”. Usedistroless/base(for glibc) oralpine(for musl). - Add a
.dockerignorecontaining at least.gitand your local build artifacts so the build context stays small and module caching is not invalidated by unrelated files. - A typical static Go service image built this way lands in the 5–15 MB range, versus hundreds of megabytes for a single-stage build on the full
golangimage. - For reproducible builds, pin the Go version explicitly (for example
1.22) rather than using a floatinglatesttag.