Day 111 — Project: Dockerfile & CI¶
Month 4 · Week 4 · ⬅ Day 110 · Day 112 ➡ · Journal index
🎯 Learning Objective¶
Package the capstone as a small, secure container with a multi-stage Dockerfile, and run a CI pipeline (build, vet, test, lint) on every push so regressions are caught automatically.
📚 Topics¶
- Multi-stage Docker builds: compile in a Go image, ship a tiny runtime image
- Static binaries (
CGO_ENABLED=0), distroless/scratch, non-root user - GitHub Actions: matrix builds, caching,
go vet/go test -race/golangci-lint - Reproducibility: pinned base images,
go.sumintegrity
📖 Reading / Sources¶
📝 Notes¶
- A multi-stage build uses a fat
golangimage to compile, thenCOPY --from=buildonly the binary into a minimal final image. The toolchain never ships to prod → smaller, smaller attack surface → [[multi-stage-build]]. - Build a static binary with
CGO_ENABLED=0 go buildso it runs onscratch/distrolesswith no libc. Pure-Go (database/sql+ pgx) needs no CGO; thepq/sqlite cgo drivers would → [[static-binary]]. - Run as non-root (
USER nonroot) and prefer distroless (no shell, no package manager) to shrink what an attacker can do inside the container. - Use a
.dockerignore(drop.git, tests, local env) so the build context is small and secrets don't leak into layers. - Layer caching:
COPY go.mod go.sumandgo mod downloadbefore copying source, so dependency layers cache across code changes and rebuilds are fast → [[build-cache]]. - CI gates on every push/PR:
go build ./...,go vet ./...,go test -race ./..., andgolangci-lint run. The-raceflag catches data races that unit tests alone miss → [[race-detector]]. - Cache the Go build/module cache in CI (
actions/setup-godoes this) so pipelines are fast. Pin action and image versions (by tag or digest) for reproducible builds. - Tag/pin base images by digest in production;
latestis not reproducible and can change under you.
💻 Code Examples¶
# ---- build stage: full toolchain ----
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download # cached unless deps change
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app ./cmd/api
# ---- runtime stage: tiny, non-root ----
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
# .github/workflows/ci.yml — runs on every push/PR
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.23', cache: true }
- run: go vet ./...
- run: go test -race -count=1 ./...
No runnable example: Docker/Actions are out-of-band tooling, not Go code. These are the actual files committed to the project repo.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| (Concept) write the multi-stage Dockerfile + CI workflow | ✅ | snippets above |
Re-run week-4 packages under go test -race |
✅ | exercises/month-04/week-4 |
🐛 Mistakes Made¶
- First image was 900 MB (shipped the whole
golangimage). Multi-stage + distroless dropped it to ~15 MB. - Built with default CGO and got "no such file or directory" on
scratch(dynamic libc). SetCGO_ENABLED=0for a static binary.
❓ Open Questions¶
- distroless
static:nonrootvsalpinefor debuggability — alpine has a shell but a bigger surface. Use distroless for prod, alpine only when I need toexecin?
🧠 Active Recall (answer without looking)¶
- Q: Why copy
go.mod/go.sumand rungo mod downloadbefore copying the source in a Dockerfile?
A
To cache the dependency-download layer. Source changes far more often than deps, so this layer is reused and rebuilds only re-download when go.mod/go.sum change.- Q: Why run
go test -racein CI even if all tests pass locally without it?
A
The race detector instruments memory access to catch data races that ordinary test runs miss (they're timing-dependent and may pass by luck). CI is where you afford the slower, instrumented run.🪶 Feynman Reflection¶
A multi-stage build is like baking in a fully-equipped kitchen, then selling only the finished cake in a plain box — the ovens and flour (the Go toolchain) never go to the customer. CI is the quality-control line: every cake is weighed, vetted, and stress-tested (race detector) before it can leave the factory, so a bad batch never reaches a release.
🕳️ Knowledge Gaps¶
- Supply-chain hardening: SBOMs, image signing (cosign), and
govulncheckin CI — add next iteration.
✅ Summary¶
I can containerise the service as a tiny, non-root, static image via multi-stage build, and gate every push on build/vet/race-test/lint in GitHub Actions.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 112: Month 4 review, polish the project README, and tag the
v0.4.0release.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: build(project): multi-stage Dockerfile and CI pipeline (day 111)