Belajar Docker - Studi Kasus Production-Grade Architecture & Checklist
Episode 22 of 28

Belajar Docker - Studi Kasus Production-Grade Architecture & Checklist

Merakit semua materi menjadi satu arsitektur production-grade: full-stack microservices (frontend, API, PostgreSQL, Redis, Traefik) plus monitoring cAdvisor, Prometheus, dan Grafana dalam satu Compose produksi lengkap, dengan checklist kesiapan produksi dan troubleshooting masalah nyata.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 21 sebelumnya kita membangun workflow development yang mulus — bind mount, hot reload, dev containers — kini saatnya merakit semua yang sudah kalian pelajari sejak episode 0 menjadi satu kesatuan: arsitektur production-grade yang nyata. Selama ini kita membahas konsep per konsep: Dockerfile multi-stage (episode 7), volumes (episode 8), networking (episode 9), Compose (episode 10–11), security (episode 13), observability (episode 15), dan reverse proxy (episode 20). Episode ini adalah tempat mereka saling bertemu dan bekerja sama.

Banyak orang bisa menjalankan docker compose up -d untuk aplikasi sederhana, tapi dunia kerja menuntut lebih: service yang tahu kapan dirinya sehat, sumber daya yang dibatasi agar satu service tak memakan semuanya, jaringan yang dipisah agar API tidak bocor ke publik, dan monitoring yang memberi peringatan sebelum kerusakan meluas. Episode ini memberikan dua hal sekaligus: sebuah studi kasus lengkap yang bisa kalian tiru, dan sebuah checklist kesiapan produksi yang bisa kalian pakai untuk mengaudit deployment kalian sendiri. Di akhir, kita juga membedah troubleshooting masalah yang paling sering membangunkan engineer di tengah malam.

Pembahasan Utama

Arsitektur Studi Kasus

Kita akan membangun platform toko online sederhana dengan arsitektur microservices yang umum ditemui di dunia nyata. Enam komponen utama, masing-masing mengajarkan satu pelajaran tersendiri:

Arsitektur full-stack production
                    Internet (80/443)
                         |
                     [ Traefik ]
                    /      |      \
            app.example  api.example  monitor.example
                   |        |            |
              [frontend] [api]       [grafana]
              Next.js    Node/Go       metrik
              port 3000  port 8000   port 3000
                   |        |
                   |    [redis] (cache)
                   |        |
                   |   [postgres] (volume)
                   |
        [cadvisor] ---> [prometheus] ---> [grafana]
  • Frontend (Next.js) — di network frontend bersama Traefik.
  • API (Node.js/Go) — di kedua network: frontend (diterima Traefik) dan backend (berbicara dengan database/cache).
  • PostgreSQL — persistent volume, hanya di network backend.
  • Redis — cache, hanya di network backend.
  • cAdvisor + Prometheus + Grafana — network monitoring, mengumpulkan metrik dari host Docker dan service API.

Perhatikan pola keamanannya: database tidak pernah berada di network yang sama dengan Traefik. Satu-satunya jalur menuju PostgreSQL adalah lewat API di network backend. Ini adalah penerapan network segmentation — konsep yang terasa abstrak di episode 9, dan kini terlihat kepentingannya: bahkan jika frontend dibobol, penyerang tidak punya jalan langsung ke database.

Compose Produksi Lengkap

Berikut file compose.prod.yaml yang merangkum seluruh pembelajaran kita. Perhatikan bukan hanya apa yang didefinisikan, tapi mengapa setiap bagian ada:

compose.prod.yaml
services:
  traefik:
    image: traefik:v3.1
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/letsencrypt/acme.json
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
      - --certificatesresolvers.letsencrypt.acme.email=ops@example.com
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
    networks:
      - frontend
 
  frontend:
    image: ghcr.io/arman/web-prod:1.4.0
    restart: always
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.frontend.rule=Host(`app.example.com`)"
      - "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
      - "traefik.http.services.frontend.loadbalancer.server.port=3000"
    depends_on:
      api:
        condition: service_healthy
    environment:
      - NEXT_PUBLIC_API_BASE=https://api.example.com
    networks:
      - frontend
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.50"
 
  api:
    image: ghcr.io/arman/api-prod:1.4.0
    restart: always
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    env_file:
      - .env.prod
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api.tls.certresolver=letsencrypt"
      - "traefik.http.services.api.loadbalancer.server.port=8000"
    networks:
      - frontend
      - backend
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.75"
        reservations:
          memory: 256M
 
  postgres:
    image: postgres:16-alpine
    restart: always
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: "1.0"
 
  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --appendonly yes
    read_only: true
    volumes:
      - redisdata:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: "0.25"
 
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    restart: always
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      - monitoring
 
  prometheus:
    image: prom/prometheus:v2.53.0
    restart: always
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - promdata:/prometheus
    networks:
      - monitoring
      - backend
    deploy:
      resources:
        limits:
          memory: 256M
 
  grafana:
    image: grafana/grafana:11.2.0
    restart: always
    volumes:
      - grafanadata:/var/lib/grafana
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`monitor.example.com`)"
      - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"
    networks:
      - monitoring
      - frontend
    deploy:
      resources:
        limits:
          memory: 256M
 
volumes:
  pgdata:
  redisdata:
  promdata:
  grafanadata:
 
networks:
  frontend:
  backend:
  monitoring:

Konfigurasi scraping Prometheus disimpan terpisah. Catat bahwa Prometheus bisa menjangkau api:8000 karena ia berada di network backend, dan menjangkau cadvisor:8080 lewat network monitoring:

prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s
 
scrape_configs:
  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]
 
  - job_name: "api"
    metrics_path: /metrics
    static_configs:
      - targets: ["api:8000"]

Perhatikan pola penting dalam compose.prod.yaml di atas:

  • Healthcheck di setiap service — bukan sekadar formalitas. depends_on dengan condition: service_healthy membuat urutan startup mengikuti kesiapan, bukan sekadar "container jalan". API menunggu PostgreSQL dan Redis benar-benar menerima koneksi sebelum mulai.
  • Resource limits — setiap service dibatasi memori dan CPU lewat deploy.resources.limits. Satu kebocoran memori di API tidak akan melumpuhkan host: container akan di-kill oleh kernel (OOM), bukan membuat seluruh server swap mati. reservations pada API menjamin baseline.
  • restart: always — Docker akan menghidupkan ulang container yang crash, kecuali dihentikan manual. Tanpa ini, satu exception bisa membuat service mati selamanya.
  • Hardening securityread_only: true (root filesystem hanya-baca), cap_drop: ALL (buang semua Linux capabilities), dan no-new-privileges diterapkan pada frontend, API, dan Redis. Ini adalah praktik dari episode 13. Redis bisa read_only karena datanya di volume; hanya direktori data yang perlu ditulis.
  • env_file: .env.prod — kredensial tidak pernah hardcode di image (yang bisa diekstrak siapa saja) maupun di file compose yang masuk git. Nilai database mengambil ${VAR} dari file .env lokal yang isi aslinya hanya ada di server.
  • Network topology — tiga network terpisah; tiap service hanya mendapat akses yang benar-benar dibutuhkan.

Perhatikan juga dua detail yang mudah terlewat. frontend tidak perlu berada di network backend sama sekali — ia cukup tahu URL publik API lewat NEXT_PUBLIC_API_BASE, sehingga bahkan jika frontend berhasil dibobol, penyerang tidak memiliki jalur jaringan menuju PostgreSQL atau Redis. Sebaliknya, prometheus sengaja ikut di network backend agar bisa men-scrape metrik api:8000 — ini satu-satunya "tamu luar" yang diizinkan masuk ke network tersebut. Pola network terkecil yang memadai ini adalah wujud konkret prinsip least privilege di level jaringan, dan jauh lebih mudah dijaga daripada firewall antar aplikasi.

Note

Satu-satunya service yang berjalan privileged: true adalah cAdvisor — dan itu disengaja: ia perlu membaca metrik kernel dan cgroups dari host (/sys, /proc, /var/lib/docker). Ini pengecualian yang sadar dan terbatas, bukan kecerobohan. Jika tim kalian keberatan dengan mode privileged, pertimbangkan mengekspos metrik Docker daemon langsung (episode 15) dan men-scrape itu saja.

Production Readiness Checklist

Mari terjemahkan studi kasus di atas menjadi checklist yang bisa kalian gunakan untuk mengaudit deployment apa pun. Ini bukan daftar teori — setiap item lahir dari insiden nyata di lapangan:

  1. Security — Jalankan sebagai non-root (USER node di Dockerfile), cap_drop: ALL, read_only: true (tambah tmpfs untuk /tmp), tidak ada secret di image, dan image memakai base minimal (Alpine/distroless). Audit dengan Trivy di CI (episode 14).
  2. Multi-stage builds — Image produksi hanya berisi runtime dan artefak, bukan compiler/dev dependency (episode 7).
  3. Resource limitsdeploy.resources.limits untuk memori dan CPU di setiap service; tambah pids-limit untuk menghentikan fork bomb.
  4. Healthchecks — Setiap service punya healthcheck; dependensi antar service memakai condition: service_healthy.
  5. Log rotation — Konfigurasi log-driver dan log-opts di daemon.json (max-size: "10m", max-file: "3"), atau logging driver terpusat (episode 15).
  6. Volume backups — Data di named volume, bukan layer tulis. Jadwalkan backup pg_dump melalui container sementara dan simpan di luar host.
  7. Zero-downtime strategy — Pola rolling restart (episode 20) atau Swarm update; pastikan aplikasi graceful shutdown.
  8. Pemantauan — cAdvisor + Prometheus + Grafana (seperti di atas), dengan alerting untuk disk, CPU, dan kesehatan service.

Troubleshooting Masalah Production

Sekalipun checklist lengkap, insiden tetap terjadi. Yang membedakan engineer senior adalah kecepatan dan ketenangan dalam mendiagnosis. Berikut lima masalah paling umum dan cara membedahnya:

1. Container restart-loop (CrashLoopBackOff-nya Docker). Gejala: docker ps menunjukkan status Restarting dengan hitungan restart yang naik terus. Langkah pertama: docker logs --tail 50 <container> untuk melihat error terakhir — log adalah saksi paling jujur. Jika log kosong atau terpotong, periksa exit code dan pesan error: docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' <container>. Kode keluar 137 berarti di-kill karena OOM (naikkan memory limit), 1/2 biasanya error aplikasi (env salah, migrasi gagal), dan 127 berarti command tidak ditemukan di image. Batasi siklus restart dengan restart: on-failure:5 agar host tidak berdegradasi.

2. docker inspect + logs sebagai urutan diagnosis. Aturan praktis: selalu mulai dari docker logs (apa yang dicetak aplikasi), lanjut ke docker inspect (metadata: exit code, env, mount, network), lalu docker exec hanya jika perlu melihat kondisi runtime. Diagnosis dari luar ke dalam, jangan langsung menyalahkan kode.

3. Disk penuh. Gejala klasik: container berhenti menulis log, database error "No space left on device". Periksa docker system df — ini memisahkan pemakaian images, containers, local volumes, dan build cache. Penyebab tersering: log yang tidak dibatasi (ingat checklist #5) dan image tak terpakai. Bersihkan dengan docker system prune -af untuk image/container, atau --volumes jika yakin data tak dibutuhkan. Ukuran volume database perlu perhatian khusus: df -h /var/lib/docker mengungkap apakah named volume memenuhi disk.

4. Port conflict. Gejala: docker compose up -d gagal dengan "port is already allocated". Temukan penyakunya dengan ss -tlnp | grep :8080 atau docker ps untuk melihat kontainer lain yang memakai port itu. Sering kali ini kontainer sisa dari percobaan lama — berhentikan dengan docker rm -f jika memang tidak terpakai, atau pindahkan port mapping service baru.

5. DNS resolution antar kontainer. Gejala: aplikasi error "getaddrinfo ENOTFOUND postgres" padahal semuanya tampak naik. Penyebabnya hampir selalu dua: (a) nama service salah ketik, atau (b) kontainer berada di network berbeda. Periksa network tiap kontainer dengan docker inspect -f '{{json .NetworkSettings.Networks}}' <container>, dan pastikan keduanya berbagi network yang sama. Uji konektivitas langsung: docker exec api getent hosts postgres — jika mengembalikan IP, DNS jalan; jika tidak, kontainer tidak sekontainer-network.

6. 502/504 Bad Gateway. Gejala: browser menampilkan 502 sementara kontainer terlihat "berjalan normal". Penyebab hampir selalu berada di lapisan komunikasi proxy→service, bukan di aplikasi itu sendiri: nama service yang tidak cocok, port internal di label Traefik atau upstream NGINX tidak sesuai dengan port yang dibuka aplikasi, atau service belum siap menerima koneksi saat request pertama tiba. Periksa docker logs pada service tujuan — jika tidak ada satupun request yang tercatat, berarti proxy tidak menemukannya sama sekali: cek label traefik.http.services.*.loadbalancer.server.port atau blok upstream, dan pastikan aplikasi bind ke 0.0.0.0, bukan hanya localhost — banyak framework secara default hanya mendengarkan loopback, sehingga koneksi dari proxy (yang datang lewat IP container) ditolak.

Warning

Jangan pernah menyelesaikan masalah DNS dengan network_mode: host atau memasang container di --network host tanpa alasan kuat. Solusi yang benar adalah memastikan semua service yang perlu berkomunikasi berada di custom bridge network yang sama. "Akses langsung ke host" menghancurkan isolasi network yang kita bangun dengan susah payah dan membuka permukaan serangan baru.

Checklist Ringkas

Sebagai penutup teknis, berikut ringkasan satu pandangan:

AspekWajibBukti Kepatuhan
SecurityNon-root, cap-drop, read-onlyDockerfile USER, cap_drop: ALL
ImageMulti-stage, base minimalImage < 200MB, tanpa compiler
ResourceMemory & CPU limitsdeploy.resources.limits
KesehatanHealthcheck + depends_on healthyStatus healthy di docker ps
LogRotation / log drivermax-size di daemon.json
DataVolume persisten + backupdocker volume ls, cron backup
DeployZero-downtime strategyRolling update tervalidasi
ObservabilityMetrik + alertingPrometheus menampilkan semua target UP

Penutup

Pada episode 22 ini kita telah merakit seluruh materi series menjadi satu arsitektur production-grade yang nyata: frontend dan API di belakang Traefik dengan TLS otomatis, PostgreSQL dan Redis di network backend yang terisolasi, serta cAdvisor, Prometheus, dan Grafana untuk observability — semuanya dalam satu compose.prod.yaml yang penuh healthcheck, resource limits, hardening, dan network segmentation. Kita juga mengubah pengalaman itu menjadi checklist kesiapan produksi delapan poin dan membedah lima masalah produksi paling umum: restart-loop, disk penuh, port conflict, DNS, dan urutan diagnosis yang benar.

Inti yang harus kalian bawa:

  • Network segmentation — database tidak pernah se-network dengan proxy publik.
  • Healthcheck + depends_on healthy adalah fondasi urutan startup yang benar.
  • Resource limits mencegah satu service melumpuhkan seluruh host.
  • Checklist 8 poin adalah alat audit yang bisa dipakai ulang untuk deployment apa pun.
  • Diagnosis masalah: docker logsdocker inspectdocker exec, dari luar ke dalam.

Di episode 23 selanjutnya kita akan menarik lapisan paling bawah: Image Internals — bagaimana image sebenarnya tersimpan sebagai tumpukan layer read-only di atas Union File System, peran Copy-on-Write, storage driver overlay2, serta perintah lanjutan docker image save/load, export/import, dan kenapa docker commit jarang disarankan. Sampai jumpa di episode 23!

Belajar Docker - Studi Kasus Production-Grade Architecture & Checklist | Belajar Docker