Multi-stage go docker builds
Golang is supposed to be a lightweight binary once compiled, as most depenedencies are resolved during development.
Yet, quite a few docker images of Golang end up being heavier in size than they need to be. Here's a quick overview of how to acheive it.
Some basics of Go in terms of project structure and dependencies:
-
Go compiles to a static binary that includes everything needed to run an application.
-
Go mod
Multi-Stage Docker Builds
Docker allows builds to be multi-staged. i.e. In one of the stages, we can create an output and use it in other stage and export only the final stage as an image. It helps in creating build environment and runtime environment isolation.
Here's an example of a multi-stage Dockerfile:
# Stage 1: Build the application; Uses golang docker image.
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the application with optimizations
# CGO_ENABLED=0 ensures static linking; -ldflags="-s -w" to strip debug info.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o santa .
# Stage 2: Create the minimal runtime image
FROM scratch
# Copy the binary from the builder stage
COPY --from=builder /app/santa /santa
# Optional: Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Command to run
ENTRYPOINT ["/santa"]
In the above, the build stage exports a binary called santa, and in the second stage, we use scratch
, a lightweight base container to export final image.
Is scratch
for everyone? It depends. Some applications might need more linux utilities, that aren't always available as Go packages.
Managing Dependencies with Go Modules:
While gomod needs a separate post by itself, a well maintained go.mod
file ensures reproducible builds:
module github.com/yourusername/myapp
go 1.21
require (
github.com/some/dependency v1.2.3
)
Size Comparison:
Approach | Image Size |
---|---|
Go with standard base image | ~850MB |
Go with Alpine base image | ~300MB |
Go with multi-stage and Alpine | ~15MB |
Go with multi-stage and scratch | ~7MB |
Other things to keep in mind:
- Not all applications can leverage scratch but alpine-linux or other linux flavours with need-basis tools added, is likely sufficient solution.
- Sometimes, the builds are to be made for the destination architecture. Usually, Go binary doesn't care about underlying host's linux architecture.