Quenchworks

Build a Go app

Go is the cleanest case for multi-stage builds. A Go program compiles to a single static binary, so the runtime image needs nothing but that file. Build on the go image, copy the binary onto static, and the final image is little more than your program: no toolchain, no shell, no package manager.

This builds on ghcr.io/quenchworks/images/go:1.25 and ghcr.io/quenchworks/images/static. The static image is the one image in the catalog tagged :latest, because it carries no language version of its own.

The two-stage Dockerfile

# Build stage: compile a fully static binary.
FROM ghcr.io/quenchworks/images/go:1.25 AS build
WORKDIR /src
# CGO off makes the binary static so it runs on the static base with no libc.
# The root filesystem is read-only, so point the build caches at /tmp.
ENV CGO_ENABLED=0 \
GOOS=linux \
GOCACHE=/tmp/gocache \
GOMODCACHE=/tmp/gomodcache
# Download modules first so the layer caches when only code changes.
COPY go.mod go.sum ./
RUN ["go", "mod", "download"]
COPY . .
RUN ["go", "build", "-trimpath", "-ldflags=-s -w", "-o", "/out/app", "./cmd/app"]
# Runtime stage: just the binary on the tiny static base.
FROM ghcr.io/quenchworks/images/static
COPY --from=build /out/app /app
USER 1001
EXPOSE 8080
ENTRYPOINT ["/app"]

Why this stays tiny

  1. Build. The go image has the full toolchain. CGO_ENABLED=0 produces a static binary with no dynamic libc dependency, which is exactly what the static base needs. The caches go to /tmp because the build runs on a read-only root filesystem.
  2. Runtime. static is a minimal base with a nonroot user and CA certificates, and not much else. Copy the one binary in, set the entrypoint, and you are done. There is no compiler and no shell to leave behind.

Next

  • The same static base works for any language that compiles to a static binary, Rust included; build with ghcr.io/quenchworks/images/rust and copy the binary over.
  • Pin by digest so each build runs exactly the base that was scanned and signed.