Building Go Applications with Distroless Images
Created:
Comparing Methods for Minimizing Go Container Images
Reducing the size of container images leads to faster deployments, improved security, and a smaller attack surface.
One popular approach is to use Google’s Distroless images, which are designed to contain only the minimum runtime needed to execute an application.
Key characteristics of distroless:
- 🚫 No package managers (no apt/yum/apk)
- 🚫 No interactive shells (no sh/bash)
- 📦 Only includes the minimal runtime environment
Variants of Distroless Images for Go
When building Go applications, you’ll typically choose between:
gcr.io/distroless/staticgcr.io/distroless/base
The difference is documented here: Documentation for base, base-nossl and static
Summary:
| Image | Characteristics |
|---|---|
| static | Designed for fully statically-linked binaries. Includes no glibc/libssl. Smallest footprint. |
| base | Includes glibc, libssl, and other shared libs. Suitable for CGO-enabled applications. |
Building a Sample Go Application with Lightweight Images
Example Source Code
We will build a simple HTTP server in Go, using logrus for logging.
To test CGO behavior, the app performs DNS lookup via net.LookupHost.
Running go run main.go and then curl localhost:8080 returns Hello World!.
package main
import (
"fmt"
"github.com/sirupsen/logrus"
"net"
"net/http"
)
var log = logrus.New()
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
func main() {
log.SetLevel(logrus.InfoLevel)
hosts, err := net.LookupHost("localhost")
if err != nil {
log.WithError(err).Error("DNS lookup failed")
} else {
log.WithField("hosts", hosts).Info("DNS lookup succeeded")
}
log.Info("Starting server on :8080")
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.WithError(err).Fatal("Server failed to start")
}
}Build Configuration
All images are built using:
go build -ldflags="-s -w" -trimpath -o main .-ldflags="-s -w"removes debugging symbols (reduces binary size)-trimpathstrips file paths from the binary
distroless images provide a nonroot user, so we use it for runtime execution.
Usage:
docker build -t golang-example .
docker run -p 8080:8080 golang-exampleBuilding with Each Base Image
distroless (static)
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o main .
FROM gcr.io/distroless/static@sha256:765ef30a...
COPY /build/main ./main
USER nonroot
ENTRYPOINT ["./main"]Important: CGO_ENABLED=0 is required.
Without static linking, the container starts with:
exec ./main: no such file or directoryThe binary exists, but shared libs are missing because static contains no glibc.
If DNS lookup via net is removed, CGO-less builds may work without issue.
distroless (base)
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -trimpath -o main .
FROM gcr.io/distroless/base@sha256:76acc04...
COPY /build/main ./main
USER nonroot
ENTRYPOINT ["./main"]Because base includes glibc, CGO can remain enabled (no need of CGO_ENABLED=0).
Alpine
Uses musl instead of glibc → static build required(CGO_ENABLED=0 is needed).
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o main .
FROM alpine:3.21.3
COPY /build/main ./main
ENTRYPOINT ["./main"]Debian-Slim
Includes glibc → works with CGO enabled (no need of CGO_ENABLED=0).
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -trimpath -o main .
FROM debian:12.10-slim
COPY /build/main ./main
ENTRYPOINT ["./main"]Scratch
Contains nothing at all (includes glibc).
Requires statically-linked binaries (CGO_ENABLED=0 is needed).
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o main .
FROM scratch
COPY /build/main ./main
ENTRYPOINT ["./main"]Image Size Comparison
Built from the same sample app:
| Base Image | Size | Requires CGO_DISABLED? | Notes |
|---|---|---|---|
| distroless/static | 8.3MB | ✔ Required | Minimal, static-only |
| distroless/base | 27.1MB | ❌ | Includes glibc |
| alpine:3.21.3 | 13MB | ✔ | musl-based |
| debian:12.10-slim | 80MB | ❌ | Small Debian base |
| scratch | 5.1MB | ✔ | Smallest, very limited |
| golang:1.23.1-bookworm | 843MB | ❌ | Build-stage only |
Scratch is the smallest, while distroless/static is tiny with better practicality. The full Go image is massive but only intended for building.
Notes on CGO Libraries (e.g., mattn/go-sqlite3)
mattn/go-sqlite3 requires CGO_ENABLED=1 and a compiler toolchain.
In README,
Important: because this is a CGO enabled package, you are required to set the environment variable CGO_ENABLED=1 and have a gcc compiler present within your path.
Example Dockerfile for static CGO builds on distroless/static:
FROM golang:1.23.1-bookworm AS builder
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libc6-dev libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -ldflags='-s -w -extldflags "-static"' -trimpath -o main .
FROM gcr.io/distroless/static@sha256:765ef30a...
COPY /build/main ./main
USER nonroot
ENTRYPOINT ["./main"]For distroless/base, no special static flags are required.
Choosing the right base image for deploying Go applications via Docker
While this example focused on go-sqlite3, many other libraries require CGO and involve complex build processes. If a slightly larger footprint (~20MB) is acceptable, distroless/base is generally the most reliable option.
Using distroless/base future-proofs the application against potential CGO dependencies. If minimizing image size is the top priority, static or scratch are better alternatives. However, if the environment requires a shell or other utilities, debian-slim remains the standard choice.