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
# 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"].git
bin/
*.test
.env
Dockerfile
.dockerignoreBefore you build
- Put your main package in ./cmd/server, or change the build path in the go build line.
- Avoid cgo dependencies, because the binary is built with CGO_ENABLED=0.
- Make the server listen on :8080 or read the port from an environment variable.
- Commit go.mod and go.sum so the module download layer can be cached.
How this Dockerfile works
FROM golang:1.25-alpine AS buildThe build stage contains the full Go toolchain, which is far too large to ship, so it is discarded after compilation.
COPY go.mod go.sum ./ RUN go mod downloadDownloading modules before copying source caches dependencies, so only code changes trigger recompilation and dependency changes trigger a fresh download.
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/serverDisabling 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.
FROM gcr.io/distroless/static-debian12:nonrootDistroless 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.
COPY --from=build /out/app /appThe compiled binary is the only file copied into the runtime image.
USER nonroot:nonrootThe nonroot tag already defaults to this user. Setting it explicitly documents the intent and guards against someone swapping in a different base tag.
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
docker build -t go-app .
docker run --rm -p 8080:8080 go-appBuild 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| Alpine runtime (shell available) | alpine:3.22 | 14.8 MB | Passing |
| Distroless static (nonroot) | gcr.io/distroless/static-debian12:nonroot | 7.7 MB | Passing |
| Scratch (static, nonroot UID) | scratch | 6.2 MB | Passing |
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.