Docker Compose
& Operasi Stack Lokal
Tiga primitif runtime dari Chapter 3 kini dirangkai jadi satu stack deklaratif: API, PostgreSQL, Redis, dan migrasi dalam satu file, lengkap dengan healthcheck yang menggerbangi urutan start dan alat untuk mengoperasikannya.
Di Chapter 3 kita menyiapkan config, jaringan, dan volume satu per satu lewat flag docker run yang panjang. Chapter ini menyatukan semuanya: Compose mendeklarasikan seluruh stack dalam satu file, lalu kita pelajari cara mengoperasikan stack itu sehari-hari, membaca log, masuk ke container untuk debug, dan membatasi resource agar satu container tidak menjatuhkan host.
Docker Compose: Multi-Container Stack
Satu compose.yaml plus healthcheck dan startup order
Compose menggantikan deretan perintah docker run panjang dengan satu file YAML yang mendeskripsikan seluruh stack lokalmu.
Aplikasi backend nyata jarang berdiri sendiri. Proyek github.com/kamu/skincare-backend butuh API Go, PostgreSQL untuk data, Redis untuk cache, dan satu langkah migrasi skema. Menjalankan semuanya manual dengan docker run dan --network yang benar itu repetitif dan rawan salah. Docker Compose mendefinisikan services, masing-masing dengan build/image, ports, environment, depends_on, volumes, dan networks, semuanya dalam satu compose.yaml.
Jika di JS kamu terbiasa membuka tiga terminal (npm run dev, server db, worker) atau di Laravel memakai Sail, Compose adalah satu file deklaratif yang menggantikan ritual itu; docker compose up menyalakan seluruh stack sekaligus.
flowchart LR M[migrate one-off] -->|run sekali| DB[(postgres)] API[api Go] --> DB API --> R[(redis)] API -.depends_on service_healthy.-> DB
Topologi stack lokal. API bicara ke PostgreSQL dan Redis; migrasi jalan sekali sebelum API melayani trafik.
compose.yaml lengkap
compose.yamlservices: postgres: image: postgres:17 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: rahasia POSTGRES_DB: skincare volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 5 redis: image: redis:7 migrate: image: migrate/migrate depends_on: postgres: condition: service_healthy volumes: - ./migrations:/migrations command: ["-path", "/migrations", "-database", "postgres://postgres:rahasia@postgres:5432/skincare?sslmode=disable", "up"] api: build: . ports: - "8080:8080" environment: DATABASE_URL: postgres://postgres:rahasia@postgres:5432/skincare?sslmode=disable REDIS_ADDR: redis:6379 depends_on: postgres: condition: service_healthy volumes: pgdata:
Perhatikan tidak ada field version: di atas; field itu kini obsolete dan diabaikan, gaya Compose modern cukup mulai langsung dari services:. Layanan saling memanggil lewat nama (postgres, redis) karena Compose membuat jaringan default dengan DNS internal antar-service, persis user-defined network yang kita buat manual di Chapter 3, kini otomatis.
depends_on tidak menjamin “siap menerima koneksi”
Ini sumber bug yang paling sering menggigit. depends_on polos hanya menjamin container PostgreSQL sudah started, bukan bahwa proses di dalamnya sudah ready menerima koneksi. PostgreSQL butuh beberapa detik untuk inisialisasi sebelum membuka socket. Tanpa pengaman, API-mu menembak database yang belum siap dan langsung mati.
depends_on saja hanya menunggu container hidup; tambahkan healthcheck pada postgres plus condition: service_healthy agar Compose menunggu sampai pg_isready lulus, dan tetap pasang retry koneksi di aplikasi sebagai jaring pengaman.
internal/db/connect.gofunc Connect(ctx context.Context, url string) (*pgxpool.Pool, error) { var pool *pgxpool.Pool var err error for attempt := 1; attempt <= 5; attempt++ { pool, err = pgxpool.New(ctx, url) if err == nil && pool.Ping(ctx) == nil { return pool, nil } time.Sleep(time.Duration(attempt) * time.Second) } return nil, fmt.Errorf("gagal konek db setelah retry: %w", err) }
Selain condition, Compose mendukung restart: true di bawah depends_on: bila sebuah dependensi (mis. postgres) di-restart lewat operasi Compose, service yang bergantung padanya ikut di-restart otomatis. Berguna agar API menyambung ulang ke database yang baru naik, bukan tergantung pada retry aplikasi saja.
Menjalankan stack
docker compose up -d membangun image API, menarik image lain, lalu menjalankan semua service sesuai urutan dependensi.
docker compose logs -f api mengikuti output API secara live untuk memastikan ia terhubung ke database dan Redis.
docker compose down -v menghentikan dan menghapus container beserta named volume pgdata, mengembalikan stack ke kondisi bersih.
Tambahkan -v pada down hanya saat kamu memang ingin membuang data; tanpa -v, volume pgdata tetap aman dan data PostgreSQL bertahan antar-restart.
Stack sudah menyala. Pertanyaan berikutnya yang pasti datang: bagaimana cara membaca apa yang terjadi di dalamnya saat ada yang salah?
Logs, Exec, dan Resource
stdout/stderr, debugging interaktif, dan batas resource
Container yang baik tidak menulis log ke file, ia mencetak ke stdout dan membiarkan platform yang mengumpulkannya.
Di dunia container, konvensinya jelas: aplikasi menulis log ke stdout dan stderr, bukan ke file di dalam container. Alasannya praktis. Writable layer container itu fana (ingat Chapter 1 dan 3), jadi log yang ditulis ke file ikut hilang saat container dibuang. Dengan mencetak ke stdout/stderr, log otomatis ditangkap oleh Docker, CI, dan platform cloud, lalu bisa dibaca dengan satu perintah seragam.
Jika kamu terbiasa console.log yang mendarat di file atau storage/logs/laravel.log ala Laravel, dalam container lupakan file itu; cukup tulis ke stdout (di Go cukup log.Print atau logger terstruktur ke os.Stdout) dan platform yang mengurus pengumpulan serta rotasi.
Membaca log dan exit code
Terminaldocker compose logs -f api # ikuti log API live docker logs --tail 50 db # 50 baris terakhir container db docker inspect -f '{{.State.ExitCode}}' api # exit code terakhir
Exit code memberi sinyal cepat: 0 artinya keluar normal, 1 error aplikasi, 137 umumnya berarti container di-kill karena melewati batas memori (OOM). docker inspect membuka seluruh metadata container, dari status, mount, sampai konfigurasi jaringan.
Skenario: API Go gagal konek database
Misalkan docker compose logs api menampilkan gagal konek db setelah retry. Langkah debug interaktif: masuk ke container dan periksa lingkungannya dari dalam.
Terminaldocker exec -it api sh # masuk shell container api env | grep DATABASE_URL # cek env yang benar-benar terbaca docker exec -it db psql -U postgres -d skincare -c '\dt' # uji koneksi dari sisi db
Pola ini menyingkap penyebab umum: DATABASE_URL menunjuk localhost (salah, harusnya nama service postgres), password tidak cocok, atau database memang belum siap. docker exec menjalankan perintah baru di container yang sudah hidup, sementara flag -it memberi terminal interaktif.
docker exec cocok untuk mengintip kondisi sesaat, tapi bukan cara merawat container produksi jangka panjang; perubahan yang kamu lakukan dari dalam hilang saat container diganti, jadi perbaikan sejati selalu lewat image atau konfigurasi.
Batas resource dan OOM
Container bukan VM, ia berbagi CPU dan memori host yang sama. Tanpa batas, satu container bocor memori bisa menjerumuskan seluruh mesin. Docker memberi rem lewat --memory dan --cpus. Saat pemakaian memori melewati batas, kernel melakukan OOM kill dan container mati dengan exit code 137.
Terminaldocker run --rm --memory=128m --cpus=1.5 \ ghcr.io/kamu/skincare-backend:1.0.0
Di Compose, batas serupa diatur lewat deploy.resources.limits (atau mem_limit untuk gaya ringkas). Menetapkan batas sejak development membantu menemukan kebocoran lebih awal, sebelum tagihan produksi yang mengingatkanmu.
compose.yamlservices: api: build: . deploy: resources: limits: memory: 256m cpus: "1.5"
Inilah toolkit operasi harianmu: baca log dari stdout, masuk dengan exec saat perlu menyelidiki, dan pasang batas resource agar host aman.
Ringkasan
Merakit dan mengoperasikan stack lokal
Chapter ini menyatukan tiga primitif runtime menjadi satu stack deklaratif, lalu memberi alat untuk mengoperasikannya saat ada yang salah.
Kita mulai dari Compose yang mengganti deretan docker run dengan satu compose.yaml: service saling memanggil lewat nama di jaringan default, healthcheck plus condition: service_healthy menggerbangi urutan start, dan migrasi jadi langkah one-off yang selesai sebelum API naik. Lalu operasi: log ke stdout dibaca lewat docker compose logs, exit code (137 menandai OOM) dibaca lewat docker inspect, docker exec -it untuk inspeksi sesaat, dan --memory/--cpus membatasi pemakaian agar host tetap aman.
Yang Wajib Menempel
- Compose mendeklarasikan seluruh stack dalam satu file;
docker compose upmenyalakannya, service saling memanggil lewat nama via jaringan default. - Gaya Compose modern tanpa field
version:; cukup mulai dariservices:. depends_onpolos hanya menunggu container started; pakaihealthcheck+condition: service_healthyagar menunggu sampai benar-benar ready, plus retry di aplikasi.restart: truedi bawahdepends_onme-restart service dependen otomatis saat dependensinya di-restart.- Tulis log ke stdout/stderr; baca lewat
docker compose logs, dandocker inspectuntuk exit code (137 = OOM kill). docker exec -ituntuk inspeksi sesaat;--memory/--cpusataudeploy.resourcesmelindungi host dari container yang rakus.
Stack jalan mulus di laptop. Tapi image-nya masih lokal. Chapter 5 membawanya keluar: memberi tag dan mendistribusikannya lewat registry secara reproducible, lalu mengeraskan image produksi agar kecil, non-root, dan bebas secret.