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.

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.
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:
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 bersama Traefik.frontend (diterima Traefik) dan backend (berbicara dengan database/cache).backend.backend.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.
Berikut file compose.prod.yaml yang merangkum seluruh pembelajaran kita. Perhatikan bukan hanya apa yang didefinisikan, tapi mengapa setiap bagian ada:
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:
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:
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.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.read_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.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.
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:
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).deploy.resources.limits untuk memori dan CPU di setiap service; tambah pids-limit untuk menghentikan fork bomb.healthcheck; dependensi antar service memakai condition: service_healthy.log-driver dan log-opts di daemon.json (max-size: "10m", max-file: "3"), atau logging driver terpusat (episode 15).pg_dump melalui container sementara dan simpan di luar host.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.
Sebagai penutup teknis, berikut ringkasan satu pandangan:
| Aspek | Wajib | Bukti Kepatuhan |
|---|---|---|
| Security | Non-root, cap-drop, read-only | Dockerfile USER, cap_drop: ALL |
| Image | Multi-stage, base minimal | Image < 200MB, tanpa compiler |
| Resource | Memory & CPU limits | deploy.resources.limits |
| Kesehatan | Healthcheck + depends_on healthy | Status healthy di docker ps |
| Log | Rotation / log driver | max-size di daemon.json |
| Data | Volume persisten + backup | docker volume ls, cron backup |
| Deploy | Zero-downtime strategy | Rolling update tervalidasi |
| Observability | Metrik + alerting | Prometheus menampilkan semua target UP |
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:
depends_on healthy adalah fondasi urutan startup yang benar.docker logs → docker inspect → docker 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!