Go Dockerfile: Multi-Stage Build to Distroless Static (Nonroot)

This Go Dockerfile compiles a static, CGO-free binary on golang:1.25-alpine and ships it alone on gcr.io/distroless/static-debian12:nonroot. The runtime image has no shell, package manager or libc, only your binary, CA certificates and time zone data. It runs as a nonroot user and listens on port 8080. Use it for Gin, Echo, Chi or plain net/http services.

CI status
Build passing
Image size (uncompressed)
7.7 MB
Build time
30 s
Runtime image
gcr.io/distroless/static-debian12:nonroot
Port
8080
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.25-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
.dockerignore
.git
bin/
*.test
.env
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM golang:1.25-alpine AS build

    The build stage contains the full Go toolchain, which is far too large to ship, so it is discarded after compilation.

  2. COPY go.mod go.sum ./ RUN go mod download

    Downloading modules before copying source caches dependencies, so only code changes trigger recompilation and dependency changes trigger a fresh download.

  3. RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

    Disabling cgo produces a fully static binary that needs no libc. -trimpath removes local paths for reproducible builds, and -s -w strip debug symbols to shrink the file.

  4. FROM gcr.io/distroless/static-debian12:nonroot

    Distroless static contains only what a static binary needs, such as CA certificates and time zone data. With no shell, an attacker has very little to work with.

  5. COPY --from=build /out/app /app

    The compiled binary is the only file copied into the runtime image.

  6. USER nonroot:nonroot

    The nonroot tag already defaults to this user. Setting it explicitly documents the intent and guards against someone swapping in a different base tag.

  7. ENTRYPOINT ["/app"]

    Uses the exec form because distroless has no shell to interpret a command string. Your binary runs as the main process and receives stop signals directly.

Build and run

terminal
docker build -t go-app .
docker run --rm -p 8080:8080 go-app

Build from the module root, where go.mod lives. Then send a request to http://localhost:8080. You can't docker exec into a shell here, so rely on logs and HTTP probes for debugging.

Other Go Dockerfile variants

Distroless static is the smallest practical choice for pure-Go services. If you need cgo, for example for SQLite drivers, a variant on a glibc distroless or Debian slim base is required. A scratch variant trades CA certificates and time zone data for even fewer files. They appear below once verified.

VariantRuntime imageImage sizeCI
Alpine runtime (shell available)alpine:3.2214.8 MBPassing
Distroless static (nonroot)gcr.io/distroless/static-debian12:nonroot7.7 MBPassing
Scratch (static, nonroot UID)scratch6.2 MBPassing

Troubleshooting

x509: certificate signed by unknown authority
This happens on scratch images without CA certificates. The distroless static base includes them, so make sure you didn't switch the runtime stage to scratch.
exec /app: no such file or directory
The binary was built with cgo and needs libc, which distroless static lacks. Keep CGO_ENABLED=0 or move to a base image that includes glibc.
package ./cmd/server is not in std or no Go files
Your main package lives elsewhere. Update the path at the end of the go build line to the directory that contains package main.

FAQ

How do I debug a container without a shell?
Distroless also publishes debug tags that include a busybox shell. Use one temporarily for troubleshooting, or attach an ephemeral debug container in Kubernetes, and keep the nonroot tag in production.
Why distroless instead of scratch?
Scratch is completely empty. Distroless static adds CA certificates, time zone data and a nonroot user, which most HTTP services need, at almost no size cost.
Does this work for Gin, Echo or Chi?
Yes. The Dockerfile only cares that you build a static Go binary. Any pure-Go framework works as long as the server listens on the exposed port.