This episode ties everything together into a production architecture: a modular project layout, graceful shutdown, env configuration, and centralized logging, then containerization with multi-stage Docker, a CI/CD pipeline in GitHub Actions, and zero-downtime deployment on Kubernetes.

All the lessons from episodes 0 to 20 now come together. This episode 21 builds a production-ready architecture: a modular project layout, graceful shutdown, secure configuration, centralized logging, containerization with multi-stage Docker, a CI/CD pipeline in GitHub Actions, and deployment with a zero-downtime strategy.
The goal isn't just to make code run, but to make code survive in production: easy to rebuild, easy to verify by machines, easy to monitor, and easy to replace without downtime. Every part of this episode is an industry practice that complements the rest.
Production-readiness starts with the code structure. Rearrange everything you've learned into a single whole:
belajar-gin/
├── cmd/server/main.go
├── internal/
│ ├── config/
│ ├── router/
│ ├── user/
│ ├── order/
│ ├── middleware/
│ └── observability/
├── migrations/
├── Dockerfile
├── .github/workflows/ci.yml
└── go.modThe internal/ folder keeps all packages private (episode 8). migrations/ stores versioned schemas (episode 9). config/ loads the environment into a struct (episode 10), and observability/ holds metrics and tracing (episode 18). This structure isn't an absolute requirement, but it's a proven pattern.
The whole application follows the discipline you've learned: handlers don't touch the database, services carry business logic, repositories use ctx, errors flow to a single middleware, and logs are always structured. When this discipline is consistent across all domains, adding a new feature feels the same as adding an old one.
Build the image with multiple stages: one stage for compilation, one lean stage for runtime:
FROM golang:1.25 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /server /server
EXPOSE 8080
USER nonroot
ENTRYPOINT ["/server"]CGO_ENABLED=0 GOOS=linux go build ... produces a static binary with no C dependencies. The runtime stage uses a distroless nonroot image containing only the binary — small and with a minimal attack surface. The .env file is never copied; configuration enters via environment variables when the container runs.
docker build -t belajar-gin:local .
docker run --rm -p 8080:8080 \
-e PORT=8080 \
-e DATABASE_URL="postgres://arman:rahasia@host.docker.internal:5432/belajargin?sslmode=disable" \
belajar-gin:localdocker build -t belajar-gin:local . builds the image from the Dockerfile. The -e flag injects the environment — exactly the variables config.Load() reads from episode 10. Run this image anywhere: a laptop, a VM, or Kubernetes, with the same configuration.
The pipeline runs tests on every push, then builds the image on release:
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Run tests
run: go test ./...
- name: Build binary
run: go build ./cmd/serveractions/setup-go with go-version: "1.25" prepares the toolchain on the runner. go test ./... from episode 17 runs on every push and pull request, so regressions are caught before they reach production. The go build step ensures the application compiles in a clean environment.
When code is merged to main, the pipeline builds the image with a version tag and pushes it to a registry:
docker tag belajar-gin:latest ghcr.io/devvnull/belajar-gin:$VERSION
docker push ghcr.io/devvnull/belajar-gin:$VERSIONdocker push ghcr.io/devvnull/belajar-gin:$VERSION uploads the tested image. Use the commit SHA or a semver as the tag so every version can be rolled back with certainty — an image whose origin can't be traced is a liability.
Kubernetes updates pods gradually without downtime. Prepare a deployment with a RollingUpdate strategy and the probes from episode 18:
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
containers:
- name: api
image: ghcr.io/devvnull/belajar-gin:$VERSION
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /readyz
port: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080maxUnavailable: 0 and maxSurge: 1 guarantee there's always a pod serving during the update. A new pod only receives traffic after the readinessProbe against /readyz succeeds. That's the zero-downtime pattern: while one pod is updated, the others keep serving.
kubectl set image deployment/belajar-gin \
api=ghcr.io/devvnull/belajar-gin:v1.21.0
kubectl rollout status deployment/belajar-ginkubectl rollout status deployment/belajar-gin monitors the update until it completes. If something breaks, kubectl rollout undo quickly returns to the previous version. Combine it with a long grace period in the preStop hook so active requests finish before the pod is stopped — completing the graceful shutdown from episode 10.
Key takeaways:
internal/, migrations, and observability.go test ./... on every push and pull request.In the next episode, episode 22, the final episode, we'll dissect alternative ecosystems & final reflection — comparing Gin with Echo, Fiber, chi, and plain net/http, when to choose each, plus a recap and a production-grade REST API checklist to close out the Learn Gin journey.