Containerization (Docker)

Containerization: Multi-Stage Builds

Go compiles to a static binary. This is its deployment superpower. You do not need the Go compiler on your production server.

The Multi-Stage Pattern

We use a “Builder” image to compile, and a “Scratch” (or distroless) image to run.

# Stage 1: Build
FROM golang:1.26-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# CGO_ENABLED=0 is critical for static binaries (no libc dependency)
# -ldflags="-w -s" strips debug info for smaller size
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o myapp ./cmd/server

# Stage 2: Runtime
# "scratch" is an empty image (0 bytes).
# "gcr.io/distroless/static" is better (includes CA Certs and Timezone data).
FROM gcr.io/distroless/static-debian12

COPY --from=builder /app/myapp /myapp

ENTRYPOINT ["/myapp"]

Why this matters

  1. Size: The final image is ~10MB (Binary) + 2MB (OS overhead). Compared to Node/Python images (500MB+), this is tiny.
  2. Security: The runtime image has no shell (/bin/sh). Attackers cannot “shell into” your container because there is no shell.
  3. Speed: Pulling and starting a 12MB image is instant.

Gotchas

  • CA Certificates: If your app talks to HTTPS APIs, scratch will fail because it lacks Root CA certs. Use distroless/static or manually copy /etc/ssl/certs from the builder.
  • Timezones: Similarly, scratch lacks tzdata. If you rely on time.LoadLocation, copy the timezone database.