Learn Gin - Production-Ready Architecture
Series/Learn Gin/Episode 21
Episode 21 of 23

Learn Gin - Production-Ready Architecture

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Modular Architecture in Production

Summary of the Patterns Already Built

Production-readiness starts with the code structure. Rearrange everything you've learned into a single whole:

Production layout
belajar-gin/
├── cmd/server/main.go
├── internal/
   ├── config/
   ├── router/
   ├── user/
   ├── order/
   ├── middleware/
   └── observability/
├── migrations/
├── Dockerfile
├── .github/workflows/ci.yml
└── go.mod

The 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.

Consistent Discipline

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.

Containerization with Multi-Stage Docker

Multi-Stage Dockerfile

Build the image with multiple stages: one stage for compilation, one lean stage for runtime:

Dockerfile
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.

Building the Image

Build and run the image
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:local

docker 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.

CI/CD Pipeline

GitHub Actions for Build and Test

The pipeline runs tests on every push, then builds the image on release:

.github/workflows/ci.yml
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/server

actions/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.

Releasing the Image

When code is merged to main, the pipeline builds the image with a version tag and pushes it to a registry:

Tag and push the image
docker tag belajar-gin:latest ghcr.io/devvnull/belajar-gin:$VERSION
docker push ghcr.io/devvnull/belajar-gin:$VERSION

docker 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.

Zero-Downtime Deployment

Rolling Update in Kubernetes

Kubernetes updates pods gradually without downtime. Prepare a deployment with a RollingUpdate strategy and the probes from episode 18:

Deployment
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: 8080

maxUnavailable: 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.

Running an Update

Rolling update
kubectl set image deployment/belajar-gin \
  api=ghcr.io/devvnull/belajar-gin:v1.21.0
kubectl rollout status deployment/belajar-gin

kubectl 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.

Closing

Key takeaways:

  • Modular layout with separate internal/, migrations, and observability.
  • Multi-stage Docker with a lean, static, nonroot runtime image.
  • Environment variables are injected at runtime, not copied into the image.
  • CI runs go test ./... on every push and pull request.
  • Images are tagged with a traceable, rollback-able version.
  • RollingUpdate with readiness probes produces zero-downtime deployments.

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.