Stage 7 · Master
Phase 3 — User Service
Dockerization
Write a multi-stage Dockerfile that compiles with the full Go toolchain but ships a distroless runtime image with no shell, no package manager, and no root user.
Harden the Existing Image Instead of Re-teaching Multi-Stage Builds
Organization Service already established the builder/runtime split. User Service introduces a stricter runtime decision: a distroless, non-root final image. The focused change removes the shell and package manager, proves pgx can run in a static binary, and copies migrations deliberately rather than copying the whole source tree.
-COPY services/organization ./services/organization
+COPY services/user-service ./services/user-service
-RUN go work init ./services/organization ./pkg/config ./pkg/logging ./pkg/apperr
+RUN go work init ./services/user-service ./pkg/respond
-WORKDIR /src/services/organization
+WORKDIR /src/services/user-service
-RUN go build -o /out/organization ./cmd/api
+RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/user-service ./cmd/server
-FROM alpine:3.20
-RUN adduser -D -u 10000 appuser
-COPY --from=build /out/organization /usr/local/bin/organization
-USER appuser
-ENTRYPOINT ["/usr/local/bin/organization"]
+FROM gcr.io/distroless/static-debian12:nonroot
+COPY --from=build /out/user-service /user-service
+COPY --from=build /src/services/user-service/migrations /migrations
+USER nonroot:nonroot
+EXPOSE 8082
+ENTRYPOINT ["/user-service"]Organization Service's Docker lesson owns the multi-stage build contract; this diff adds a distroless nonroot runtime, the nonroot user, and migrations copied into the image for the release Job.
No shell means kubectl exec cannot give you ps, cat, curl, or a package manager during an incident. Ship a separate debug image or documented ephemeral-container workflow before production uses this runtime, otherwise the hardened image becomes an operational blind spot.
.git
*_test.go
tmp/
*.logExcluding _test.go files from the build context is optional for correctness but shrinks the context sent to the Docker daemon and keeps test-only fixtures out of the builder stage entirely.
USER_SERVICE_DATABASE_URL contains a database password. It must arrive as a runtime environment variable or a mounted Kubernetes Secret — never as a Dockerfile ARG or ENV baked at build time, where it would persist in the image's layer history and be recoverable with a plain docker history.
Adding the Service to docker-compose.yml
docker-compose.yml already orchestrates Postgres and organization-service from earlier phases. Adding user-service means one new service block, pointed at the same shared postgres container but its own database name, matching the database-per-service convention established since Phase 1.
services:
user-service:
build: ./services/user-service
environment:
USER_SERVICE_PORT: "8082"
USER_SERVICE_DATABASE_URL: postgres://hoa:hoa@postgres:5432/hoa_user?sslmode=disable
APP_ENV: development
ports:
- "8082:8082"
depends_on:
postgres:
condition: service_healthydepends_on with condition: service_healthy — not just service_started — means Compose waits for Postgres's own healthcheck to pass before starting user-service, avoiding a startup-order race that a bare depends_on would not catch.
Building and Running the Image Locally
Applied exercise
Audit an insecure Dockerfile
A teammate's draft Dockerfile has one stage: FROM golang:1.26 with ENV USER_SERVICE_DATABASE_URL=postgres://hoa:hoa@... baked in, and no USER instruction.
- List every issue you find, referencing this lesson's specific reasoning for each.
- Rank the issues by severity, with the most severe first.
- Rewrite the single worst line and explain what replaces it.
Deliverable
A ranked issue list plus one corrected line of Dockerfile.
Completion checks
- The baked-in database credential is flagged as the most severe issue.
- The missing non-root USER instruction is flagged as a distinct issue, not merged with the credential issue.
Why does the Dockerfile use CGO_ENABLED=0 when building the binary?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.