Use a multi-stage Dockerfile: a full Go builder stage that compiles the binary (embedding version/build time via ldflags), and a minimal runtime stage (scratch or distroless) that runs as a non-root user. Key rebuild-performance trick: copy only go.mod/go.sum first to leverage Docker layer cache for module downloads.
Dockerfile:
dockerfile
# builder: compile with modules cached, embed metadata
FROM golang:1.20-alpine AS builder
WORKDIR /src
# install git (for go modules) and ca-certificates for https
RUN apk add --no-cache git ca-certificates
# improve caching: copy module files first
COPY go.mod go.sum ./
RUN go mod download
# copy source
COPY . .
# build metadata (set at build-time)
ARG BUILD_VERSION=dev
ARG BUILD_TIME=unknown
# build statically, strip symbols for smaller binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w -X 'main.Version=${BUILD_VERSION}' -X 'main.BuildTime=${BUILD_TIME}'" \
-o /app/bin/myapp ./cmd/myapp
# final: minimal runtime, non-root user
FROM scratch AS runtime
# copy CA certs for TLS if needed
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# create unprivileged user and app dir in builder and copy
# (we create them in builder for ownership)
FROM builder AS prep
RUN addgroup -S appgroup && adduser -S appuser -G appgroup -h /nonroot
RUN mkdir -p /nonroot/app && chown appuser:appgroup /nonroot/app
# copy binary into a minimal busybox-like layout for ownership, then to scratch
COPY --from=builder /app/bin/myapp /nonroot/app/myapp
USER appuser
FROM scratch AS final
COPY --from=prep /nonroot /nonroot
ENV PATH=/nonroot
WORKDIR /nonroot/app
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/nonroot/app/myapp"]
Why these choices
- Multi-stage: keeps final image tiny and free of build toolchain.
- Scratch/distroless: minimal attack surface and smaller image.
- Non-root user: reduces risk if container escape occurs.
- ldflags -X: injects Version/BuildTime without runtime config; useful for support/tracing.
- go.mod/go.sum first + go mod download: caches module fetches so code changes don't re-download deps.
- CGO_ENABLED=0 and static build: allows use of scratch; avoids glibc issues.
- Stripping symbols (-s -w): reduces binary size.
- .dockerignore (exclude vendor/.git, .git, build artifacts) further speeds build.
Security/perf extras: sign images, run image scanners, set read-only filesystem and drop capabilities at runtime, and use healthchecks and small base images like distroless for required libraries.