Stage 7 · Master
Phase 13 — Staff Service
Deployment
Package staff-service with its own Dockerfile, Helm values, and path-filtered CI stage so a change to staff-service never triggers a redeploy of unrelated services.
The Image Is Boilerplate; the Copy Boundary Is Not
The multi-stage layout, the non-root runtime user, and the repository-root build context are all settled decisions from earlier phases, and staff-service's Dockerfile is those decisions with the service name substituted. Exactly one line in it is a real choice, and it is the one most likely to be written wrong.
COPY go.work go.work.sum ./
-COPY . .
+COPY pkg ./pkg
+COPY services/staff-service ./services/staff-service
RUN go build -o /out/staff-service ./services/staff-service/cmd/apiCopying the whole tree would work and would be wrong: every unrelated service's commit would invalidate this image's build cache, and a monorepo of eleven services turns that into minutes per build. Copying only go.work, pkg, and this module keeps the cache proportional to what staff-service actually depends on — and the go.work file has to come first, because the build fails without it even though nothing in this service imports it directly.
Helm Values Encode Environment, Not Code
replicaCount: 2
image:
repository: registry.meridian.internal/staff-service
tag: "" # set by CI to the commit SHA
env:
- name: STAFF_SERVICE_DB_URL
valueFrom:
secretKeyRef:
name: staff-service-db
key: url
- name: STAFF_SERVICE_PORT
value: "8087"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
migrationJob:
enabled: true
command: ["migrate", "-path", "/migrations", "-database", "$(STAFF_SERVICE_DB_URL)", "up"]The migration job runs as a Helm pre-upgrade hook, so a rollout that would apply an incompatible migration fails before any new pod receives traffic.
CI Should Not Rebuild What Didn't Change
name: staff-service
on:
push:
branches: [main]
paths:
- "services/staff-service/**"
- "pkg/**"
jobs:
build-test-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.x"
- run: go test ./services/staff-service/...
- run: docker build -t registry.meridian.internal/staff-service:${{ github.sha }} -f services/staff-service/Dockerfile .
- run: docker push registry.meridian.internal/staff-service:${{ github.sha }}
- run: helm upgrade --install staff-service services/staff-service/deploy --set image.tag=${{ github.sha }} --waitThe paths filter means a change to notification-service never triggers this workflow — and, symmetrically, a change here never triggers notification-service's own pipeline.
Because every service imports pkg, a change there must rebuild every service that depends on it. This is the one place path filtering intentionally stays broad — narrowing it would risk deploying a stale copy of shared authentication or tenant code.
Confirm the New Version Is Actually Serving Traffic
Applied exercise
Add a canary step before full rollout
A previous staff-service deploy caused a brief spike in 500s that only a full rollout revealed. Leadership wants a canary stage.
- Decide how many replicas the canary should run relative to the stable version.
- Choose a concrete signal (a metric or a set of requests) that decides whether to proceed or roll back.
- Add the canary and promotion steps to the GitHub Actions workflow.
- State what happens automatically if the canary signal looks bad — and who has to intervene manually versus what rolls back on its own.
Deliverable
An updated staff-service.yml workflow plus a short written rollback policy.
Completion checks
- The canary is a real subset of production traffic, not a separate always-green environment.
- The promotion or rollback decision is based on a stated signal, not a fixed timer alone.
Why does the GitHub Actions workflow filter on both `services/staff-service/**` and `pkg/**`?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.