# ---- Stage 1: build ---- # This stage has the full Go toolchain and is only used to compile the # binary. It is discarded entirely after the build - none of its ~800MB+ # footprint ends up in the final image. FROM golang:1.26 AS builder WORKDIR /app # Copy just the module files first and download dependencies before # copying the rest of the source. Docker caches each instruction as a # layer; as long as go.mod/go.sum don't change, this layer (and the # downloads it triggers) is reused on every subsequent build, even if # application code changes constantly. If we copied all source first, any # code edit would invalidate this cache and force a full re-download every # single build. COPY go.mod go.sum* ./ RUN go mod download COPY . . # CGO_ENABLED=0 produces a fully static binary with no dynamic library # dependencies, which is what lets it run on the minimal Alpine base image # in stage 2 below without missing shared libraries. GOOS=linux ensures we # cross-compile for Linux even if you're building this image on macOS or # Windows. RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/server ./cmd/api # ---- Stage 2: run ---- # A tiny (~7MB) base image that receives ONLY the compiled binary from # stage 1 - no compiler, no source code, no build tools. Smaller image, # smaller attack surface. FROM alpine:3.20 # Alpine's minimal base doesn't include root CA certificates by default. # Without these, any outbound HTTPS call this app makes (Google's OAuth2 # token exchange and userinfo endpoints) would fail with a certificate # verification error. --no-cache avoids leaving package-manager cache # files behind in the image. RUN apk add --no-cache ca-certificates WORKDIR /app # Pull just the compiled binary out of the builder stage - this is the # actual multi-stage mechanism: --from=builder reaches back into the FIRST # image just for this one file. COPY --from=builder /app/bin/server . EXPOSE 8080 CMD ["./server"]