Lesson 4 · Ship it — code → prod plumbing
Build system / Skaffold
How 30+ services become one binary in one image that runs as any of them — and the two very different local workflows that get it onto a cluster.
Your win: explain the single-binary model, and tell the two local build paths (Skaffold+kind vs docker-compose) apart.
One binary, all services
The monorepo compiles every service into a single server binary.
Each service is pulled in by a blank import and self-registers; the runtime picks which service to be
via a cobra subcommand:
import (
_ ".../cmd/server/eureka" // blank import → self-registers
_ ".../cmd/server/notificationmgmt"
// ... every service
)
// root cobra command "server" → two subtrees:
// gserver → run the gRPC server for a service
// gjob → run a job (sql_migrate, crons, data migrations)
gjob sql_migrate (Lesson 1) and gjob notificationmgmt_consumers (Part 3):
same binary, different subcommand.1
Cross-compiled for linux
The local build script cross-compiles for the container platform, not your Mac:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./build/server ./cmd/server/
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./build/bdd.test ./features/
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./build/stub ./features/stub/
build/server is the hot-swapped server; build/bdd.test is the BDD runner
(into gandalf); build/stub is the Learnosity mock. They only run inside a linux
container.1
Two Dockerfiles — dev copies, prod compiles
dev (development.Dockerfile → developer) | prod (release.Dockerfile → runner) | |
|---|---|---|
| base | debian:12.11 | alpine:3.21.7 |
| binary | copies the pre-built ./build/server | compiles /server inside a build stage |
| why | fast local loop (build once, copy in) | reproducible, self-contained prod image |
Both end with ENTRYPOINT ["/server"] and bundle /migrations. The dev
image trusts your local cross-compiled binary; the prod image rebuilds from source in CI.2
Two local paths — and only one is Skaffold
This trips people up: there are two independent ways to run the stack locally.
| Skaffold + kind | docker-compose | |
|---|---|---|
| launch | deployments/sk.bash → skaffold.local.yaml | ./local/run.bash (the one CLAUDE.md documents) |
| how | custom build cmd → docker build → kind load → Helm-deploy every service | compose services; hot-swap build/server into a running container |
| use | full Kubernetes-like stack | the fast inner dev loop |
skaffold.backend.yaml the top level is only deploy.helm.releases (one
per service). The actual build lives inside profiles — the
local profile uses a custom.buildCommand (not a Docker
artifact block) that builds the three binaries, docker build --target developer, tags
backend:locally, and kind loads it. It even fails on purpose if you try to
push a local image. And ./local/run.bash doesn't touch Skaffold at all.3
Skaffold — the docs
The build/tag/deploy loop, profiles, and custom build commands — match them to
skaffold.backend.yaml.
→ skaffold.dev/docs
→ in-repo cmd/server/main.go, local/build-code-golang.bash,
skaffold.backend.yaml, developments/*.Dockerfile
Check yourself (from memory)
Q1. How does one image run as many different services?
gserver/gjob) and the mounted config/secrets.
Q2. How do the dev and prod Docker images differ?
developer, debian) copies your cross-compiled
build/server; prod (runner, alpine) compiles from source in CI.
Q3. Which local path uses Skaffold?
deployments/sk.bash;
./local/run.bash is the separate docker-compose hot-swap path.
cmd/server/main.go,
blank imports self-register); runtime picks the service via a cobra subcommand
(gserver/gjob) + mounted config — same image run many times.
Cross-compile (build-code-golang.bash, GOOS=linux GOARCH=amd64)
→ build/{server,bdd.test,stub}. Two images: dev
(development.Dockerfile:developer, debian, copies prebuilt binary) vs prod
(release.Dockerfile:runner, alpine, compiles inside). Two local paths:
Skaffold+kind (deployments/sk.bash → custom build → kind load → Helm; build
lives in profiles) vs docker-compose (./local/run.bash, hot-swap the binary —
NOT Skaffold).1. In-repo: cmd/server/main.go (blank imports, server→gserver/gjob), internal/golibs/bootstrap/command.go, local/build-code-golang.bash.
2. In-repo: developments/development.Dockerfile (developer, copies prebuilt), developments/release.Dockerfile (runner, compiles inside).
3. In-repo: skaffold.backend.yaml (deploy.helm top-level; build in profiles; custom.buildCommand; push guard :317-320), skaffold.local.yaml, deployments/sk.bash, local/run.bash.