Stage 7 · Master
Containerization and Deployment
Multi-Stage Docker Builds for a Go Workspace
Why production images should contain only a compiled service binary, how multi-stage builds work, and the Dockerfile design Meridian should add in its next infrastructure phase.
Why Multi-Stage Builds Exist
A container image has two very different jobs. During build time it needs a compiler, module downloader, caches, and sometimes debugging tools. During runtime it needs only the application binary, trusted certificates, and a small amount of operating-system support. A single-stage Dockerfile mixes those jobs together and ships the entire build environment to production. Multi-stage builds separate them: one stage builds, a later stage copies only the finished artifact.
That separation matters for more than image size. Smaller runtime images pull faster, scan with less noise, and expose fewer tools to an attacker or a confused operator. The operational principle is simple: production should contain the minimum capability required to start the process and serve traffic. A compiler inside a runtime image is dead weight at best and extra attack surface at worst.
| Image style | What it includes | What goes wrong |
|---|---|---|
| Single-stage build | Go toolchain, source tree, module cache, shell utilities, final binary | Large images, noisy vulnerability reports, and accidental dependence on build-time tools in production. |
| Multi-stage build | Builder stage for compilation, minimal runtime stage for execution | Requires a little more Dockerfile discipline, but produces a cleaner and safer artifact. |
| Prebuilt binary copied by hand | Only the binary, but without a reproducible build recipe | Fast for experiments, but weak for CI because the artifact cannot be recreated deterministically. |
What the Real Repo Looks Like Today
The real Meridian repository is still in the scaffolding phase. It has a go.work file, four deployable services under apps/, one shared library under libs/platform, and no Dockerfiles yet. That current state is important because this chapter is a design specification, not a claim that the container build path already exists in the reference repository.
.dockerignorecreateResponsibility: Keeps git metadata, editor files, Next.js build output, and other irrelevant content out of every Docker build context.
Why now: Docker sends the build context before it runs any layer. A noisy context slows every build and invalidates cache layers too easily.
apps/gateway/DockerfilecreateResponsibility: Builds the gateway binary from apps/gateway/cmd/gateway and copies it into a minimal runtime image.
Why now: gateway is the only public entrypoint and must become an independently deployable image before Kubernetes manifests can target it.
apps/identity-service/DockerfilecreateResponsibility: Builds identity-service as its own immutable image.
Why now: Each service is an independent Go module today and should remain an independent release unit in production.
Connects to: Follows the same workspace-aware copy pattern as apps/gateway/Dockerfile.
apps/community-service/DockerfilecreateResponsibility: Builds community-service as a separate runtime artifact.
Why now: Residents, units, announcements, and complaints will evolve on a different cadence from identity and billing concerns.
apps/billing-service/DockerfilecreateResponsibility: Builds billing-service as an isolated runtime image.
Why now: Billing will eventually have different scaling, security, and rollout requirements from the other services.
A Workspace-Aware Dockerfile
A Go workspace adds one subtle requirement to Docker builds: the target service cannot be built as if it lives in isolation. The build context must include go.work and the shared module manifests, because apps/gateway, apps/identity-service, apps/community-service, and apps/billing-service all import libs/platform through the workspace. A Dockerfile that copies only one module directory will compile only until the first shared import appears.
# syntax=docker/dockerfile:1.7
FROM golang:1.26-bookworm AS deps
WORKDIR /workspace
COPY go.work ./
COPY libs/platform/go.mod ./libs/platform/go.mod
COPY apps/gateway/go.mod ./apps/gateway/go.mod
COPY apps/identity-service/go.mod ./apps/identity-service/go.mod
COPY apps/community-service/go.mod ./apps/community-service/go.mod
COPY apps/billing-service/go.mod ./apps/billing-service/go.mod
RUN go work sync && go mod download -C apps/gateway
FROM golang:1.26-bookworm AS build
WORKDIR /workspace
COPY --from=deps /go/pkg /go/pkg
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o /out/gateway ./apps/gateway/cmd/gateway
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=build /out/gateway /app/gateway
EXPOSE 8080
ENTRYPOINT ["/app/gateway"]The first stage resolves dependencies, the second stage compiles a static Linux binary, and the last stage contains only the runnable artifact. The binary path matches the real repository layout exactly: apps/gateway/cmd/gateway.
Cache Ordering and Reproducibility
Docker layer ordering is part of the build contract. Dependency download should depend on manifest files, not on the entire repository tree. If a Dockerfile copies . before go mod download, then an edit to README.md or a Kubernetes manifest invalidates the dependency layer and forces a full redownload. In a multi-service repository this becomes a repeated CI tax.
Runtime Hardening Rules
- Use Go 1.26 in every builder stage, matching the real go.mod files in meridian.
- Build one image per service. Do not bundle gateway and downstream services into one container.
- Run as non-root in the runtime stage.
- Keep environment-specific configuration out of the image. Ports, downstream URLs, and credentials belong at deploy time, not build time.
- Prefer immutable tags or digests in CI so the exact shipped artifact can be identified later.
A poor container artifact leaks directly into later lessons. Kubernetes probes, rollout safety, image scanning, and incident response all become harder when the runtime image is oversized, mutable, or environment-specific.
Planned File Shape
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.