Progress belajar
Modul 60 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
Docker Compose
untuk Local Stack
Satukan API, worker, PostgreSQL, dan Redis lokal agar backend skincare bisa dijalankan konsisten di semua laptop developer dengan satu perintah.
Kenapa Docker Compose?
Satu file YAML menggantikan setengah halaman README berisi langkah setup manual.
Di JavaScript kamu mungkin pernah pakai npm run dev plus sebuah database lokal yang dipasang manual. Di Laravel kamu mungkin kenal Sail. Di Go, Docker Compose memberi cara vanilla untuk menyalakan seluruh dependency lokal lewat satu file.
Di Chapter 1 kita sudah membungkus binary Go menjadi image yang kecil dan aman lewat multi-stage Dockerfile. Tapi sebuah image API saja belum membuat backend bisa jalan. Online shop skincare butuh PostgreSQL untuk katalog dan order, Redis untuk cache dan rate limit, serta worker yang memproses email verifikasi dan webhook payment di latar belakang. Menyalakan semua itu satu per satu dengan docker run panjang penuh flag adalah mimpi buruk yang berbeda di tiap laptop.
Docker Compose menyelesaikan masalah ini. Satu file mendeklarasikan semua service, network, volume, port, healthcheck, dan environment sebagai satu aplikasi. Perintah docker compose up menyalakannya, docker compose down mematikannya. Hasilnya, onboarding developer baru turun dari “ikuti 20 langkah di README” menjadi “clone repo, copy .env, jalankan satu perintah”.
Ada nilai kedua yang sering terlewat: parity. Compose membuat environment lokal mirip production dalam hal yang penting (PostgreSQL betulan, bukan SQLite; Redis betulan; worker terpisah dari API). Bug yang khas production, seperti race saat reservasi stok atau koneksi pool yang habis, jadi bisa direproduksi di laptop. “Jalan di mesinku” perlahan berubah arti dari alasan untuk menyalahkan environment menjadi sinyal bahwa kode memang siap dipromosikan.
Docker run satu container itu seperti satu pemain musik. Compose adalah partitur yang memberi tahu semua pemain (API, worker, database, cache) kapan masuk, dengan tempo yang sama, sehingga keluar sebagai satu lagu yang utuh dan bisa diulang persis.
Laravel Sail adalah pembungkus tipis di atas Compose dengan command ./vendor/bin/sail. Di Go kita menulis compose.yaml langsung, tanpa framework di tengah. Lebih eksplisit, tetapi justru lebih mudah dibawa ke worker, ke CI, dan ke pola yang sama untuk staging.
- Sail membungkus Compose dengan command
./vendor/bin/sail up. - Konvensinya nyaman untuk PHP-FPM, MySQL, Redis, dan Mailpit.
- Banyak keputusan sudah dibuatkan oleh preset Laravel.
- Kita menulis
compose.yamllangsung di root proyek. - Service
apidanworkermemakai image Go dari Dockerfile Chapter 1. - Kita menentukan sendiri readiness, env, volume, network, dan port.
Alat untuk mendefinisikan dan menjalankan beberapa container sebagai satu stack aplikasi, dijalankan lewat docker compose up dan dimatikan lewat docker compose down.
Docker Compose mendeskripsikan stack aplikasi dalam satu konfigurasi. Compose file reference memuat konsep services, networks, volumes, dan healthcheck yang dipakai sepanjang modul ini.
Anatomi Compose File Modern
Compose Spec terbaru sudah tidak memakai key version, dan nama file kanoniknya compose.yaml.
Sebelum menulis file panjang, kenali dulu empat blok besar yang menyusun setiap Compose file: services, networks, volumes, dan, opsional, secrets.
Banyak tutorial lama mengawali Compose file dengan baris version: "3.8". Itu sudah usang. Compose Spec modern (implementasi Docker Compose v2) mengabaikan key version dan bahkan memberi peringatan jika kamu menuliskannya. Nama file yang dianjurkan sekarang adalah compose.yaml (bukan docker-compose.yml), meski nama lama tetap dikenali demi kompatibilitas. Sepanjang modul ini kita pakai compose.yaml.
Jika kamu menyalin file lama yang diawali version: "3" atau version: "3.8", hapus baris itu. Compose v2 modern menganggapnya deprecated dan akan mencetak warning di setiap perintah up.
Struktur kanonik sebuah Compose file modern hanya butuh top-level key berikut. name memberi nama proyek (prefix untuk container, network, dan volume). services adalah inti: tiap entri menjadi satu container. volumes mendeklarasikan named volume agar data persist. networks mendeklarasikan jaringan internal antar service.
compose.yaml (kerangka)name: skincare-backend services: api: { } # container Go HTTP API worker: { } # container Go worker postgres: { } # database redis: { } # cache dan queue ringan volumes: pgdata: { } # data PostgreSQL persist networks: default: { } # otomatis dibuat; service saling resolve lewat nama
Di Node, package.json punya blok scripts yang memetakan nama ke perintah. Compose file mirip itu untuk infrastruktur: tiap key di services adalah “nama” yang memetakan ke sebuah container utuh, lengkap dengan image, port, dan dependensinya. Bedanya, Compose juga mengurus jaringan dan storage di antara mereka.
Di dalam satu service, ada sekumpulan key yang akan sering kamu pakai. Mengenalinya sekarang membuat file panjang di Section 05 mudah dibaca.
build vs image
build membangun image dari Dockerfile lokal. image memberi nama hasil build, atau menarik image jadi dari registry (mis. postgres:17).
command
Menimpa argumen default image. Untuk image kita, ["/app", "serve"] menjadikan satu image jalan sebagai API atau worker.
ports vs expose
ports mem-publish ke host (HOST:CONTAINER). expose hanya dokumentasi internal, tidak membuka ke host.
depends_on
Mengatur urutan start, dan dengan condition bisa menunggu service lain sehat lebih dulu.
env_file vs environment
env_file memuat banyak variabel dari file. environment menetapkan atau menimpa variabel per service.
restart
Kebijakan restart container: no, on-failure, always, atau unless-stopped yang kita pakai untuk dev.
Dari Chapter 1, image kita punya ENTRYPOINT ["/app"]. Key command di Compose menambahkan argumen ke entrypoint itu, jadi command: ["/app", "serve"] praktis menjalankan /app serve. Inilah cara satu image yang sama bisa jadi API (serve) atau worker (worker) hanya dengan mengganti command.
Spesifikasi terbuka yang mendefinisikan format Compose file. Docker Compose v2 adalah implementasinya. Spec modern menghilangkan key version dan menjadikan compose.yaml sebagai nama file kanonik.
Posisi File di Root Proyek
Compose file berada di root, sejajar dengan go.mod, Dockerfile, dan .env.
Letakkan compose.yaml di root proyek agar build.context: . bisa melihat go.mod, seluruh source, dan Dockerfile tanpa path yang berputar.
Root proyek adalah konteks build paling sederhana. Saat Compose membangun image api dan worker, ia mengirim isi direktori ini (dikurangi yang ada di .dockerignore) sebagai build context ke Docker daemon. Karena go.mod, cmd/, dan Dockerfile semua ada di sini, satu Dockerfile cukup untuk membangun kedua binary. Berikut struktur Go Artisan saat masuk Chapter 2.
- skincare-backend/
- cmd/
- api/
- main.go entry point HTTP API
- worker/
- main.go entry point background worker
- internal/
- config/ loader env dan validasi konfigurasi
- product/ domain katalog skincare
- order/ domain checkout dan order
- payment/ domain payment dan webhook
- migrations/ SQL migration lokal dan CI
- Dockerfile multi-stage dari Chapter 1
- .dockerignore cegah .env dan .git masuk image
- compose.yaml stack lokal development
- .env rahasia lokal, jangan commit
- .env.example contoh env aman untuk commit
- go.mod
- go.sum
Modul ini memakai satu image dengan subcommand: serve untuk API dan worker untuk background job. Jika kamu lebih suka dua binary (./cmd/api dan ./cmd/worker), build keduanya di Dockerfile dan rujuk binary yang tepat lewat command di tiap service.
Karena Dockerfile memakai COPY . ., tanpa .dockerignore file .env lokal bisa ikut ter-copy ke image. Pastikan .env, .git, dan bin/ masuk .dockerignore agar build cepat dan secret tidak bocor ke layer image.
Model Service Lokal
Stack lokal harus cukup mirip production agar bug transaksi dan async terlihat sejak development.
Satu stack lokal sebaiknya merepresentasikan sistem production secara dekat, tetapi tetap ringan untuk laptop. Untuk skincare, empat service sudah cukup: api, worker, postgres, redis.
API menerima HTTP request dari frontend React. Worker memproses pekerjaan async: mengirim email verifikasi, menyinkronkan payment report, atau melepas reservasi stok yang timeout. PostgreSQL menyimpan data utama (produk, cart, order). Redis dipakai sebagai cache katalog, rate limit store, atau queue ringan, sesuai kebutuhan yang muncul di roadmap security dan scaling.
flowchart LR
FE["Frontend React"] -->|HTTP JSON :8080| API["api (Go)"]
API -->|SQL via pgx| PG[("postgres")]
API -->|cache, rate limit| RD[("redis")]
API -->|enqueue job| RD
RD -->|dequeue| WK["worker (Go)"]
WK -->|update status| PG
WK -->|kirim email, sync payment| EXT["Layanan eksternal"]Gambar 1. Empat service dalam satu network Compose. API melayani request sinkron, worker mengambil job async lewat Redis.
api
Container Go HTTP API, membuka port 8080, membaca DATABASE_URL, terhubung ke postgres dan redis lewat nama service.
worker
Container Go worker dari image yang sama, command berbeda, tidak membuka port ke host karena tidak melayani HTTP.
postgres
Database lokal dengan named volume agar data tetap ada walau container dihentikan dan dibuat ulang.
redis
Cache, rate limit store, atau queue ringan, hanya terlihat di dalam network Compose, tidak di-publish ke internet.
Karena api dan worker memakai image yang sama, binary Go perlu tahu peran apa yang harus dijalankan. Pola paling sederhana adalah dispatch berdasarkan argumen pertama, sehingga command di Compose menentukan perilaku.
cmd/app/main.gopackage main import ( "context" "log/slog" "os" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) ctx := context.Background() cmd := "serve" if len(os.Args) > 1 { cmd = os.Args[1] } var err error switch cmd { case "serve": err = runAPI(ctx, logger) case "worker": err = runWorker(ctx, logger) case "migrate": err = runMigrate(ctx, logger, os.Args[2:]) default: logger.Error("unknown command", "cmd", cmd) os.Exit(2) } if err != nil { logger.Error("command failed", "cmd", cmd, "error", err) os.Exit(1) } }
Unit aplikasi di Compose yang biasanya menjadi satu container, misalnya api, postgres, atau redis. Satu service bisa di-scale menjadi beberapa container dengan flag --scale.
Aplikasi React sering cukup dengan satu dev server plus API remote. Backend Go lokal biasanya butuh database nyata, cache, dan worker, agar bug transaksi, deadlock koneksi, dan kegagalan async job bisa terlihat di laptop, bukan baru muncul di production.
compose.yaml Siap Pakai
File baseline ini bisa ditempatkan langsung di root proyek skincare.
Berikut Compose file lengkap untuk local development stack. Bacalah dari atas: dua service Go (api dan worker) dari image yang sama, lalu postgres dan redis dengan healthcheck masing-masing.
compose.yamlname: skincare-backend services: api: build: context: . dockerfile: Dockerfile image: skincare-backend:dev command: ["/app", "serve"] env_file: .env environment: APP_ENV: development HTTP_ADDR: ":8080" DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable REDIS_ADDR: redis:6379 ports: - "8080:8080" depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped worker: build: context: . dockerfile: Dockerfile image: skincare-backend:dev command: ["/app", "worker"] env_file: .env environment: APP_ENV: development DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable REDIS_ADDR: redis:6379 depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped postgres: image: postgres:17 env_file: .env volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 start_period: 30s redis: image: redis:7 command: ["redis-server", "--appendonly", "yes"] volumes: - redisdata:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 volumes: pgdata: redisdata:
Contoh ini mengasumsikan ENTRYPOINT image adalah ["/app"] (dari Dockerfile Chapter 1), dan binary mendukung subcommand serve serta worker. Jika proyekmu membangun dua binary terpisah, ubah command menjadi ["/app-api"] dan ["/app-worker"] sesuai Dockerfile.
Kita pakai postgres:17 dan redis:7 agar sejalan dengan target production di chapter RDS nanti. Per 2026 Postgres 18 dan Redis 8 juga sudah GA, jadi kamu boleh upgrade, tetapi pin tag mayor agar build reproducible dan tidak diam-diam berpindah versi.
Ada satu detail penting di healthcheck postgres. Perhatikan $${POSTGRES_USER} dengan dua dolar. Compose melakukan interpolasi variabel pada string YAML, jadi $VAR akan dicoba diganti oleh Compose. Untuk meneruskan $ apa adanya ke shell di dalam container (agar pg_isready membaca env var container), kita escape menjadi $$.
Di dalam string Compose, tulis $$ setiap kali kamu ingin sebuah $ literal diteruskan ke shell container, bukan diinterpolasi oleh Compose. Lupa ini membuat pg_isready mencoba user bernama kosong dan healthcheck gagal selamanya.
Startup Order dan Healthcheck
depends_on mengatur urutan, tetapi condition service_healthy yang membuat API menunggu DB benar-benar siap.
Container postgres bisa berstatus running padahal proses database di dalamnya masih inisialisasi. Tanpa penjaga, API Go connect terlalu cepat lalu crash saat boot. Inilah masalah yang dipecahkan oleh kombinasi healthcheck dan condition: service_healthy.
depends_on versi sederhana hanya menunggu container target start, bukan siap menerima koneksi. Untuk database, dua hal itu berbeda jauh: Postgres butuh beberapa detik untuk inisialisasi cluster dan membuka socket. Dengan menambahkan condition: service_healthy, Compose menunda start api dan worker sampai healthcheck postgres benar-benar PASS. Healthcheck postgres kita memakai pg_isready, dan redis memakai redis-cli ping.
flowchart TD
A["docker compose up --build"] --> B["Build image skincare-backend:dev"]
B --> C["Start postgres"]
B --> D["Start redis"]
C --> E{"pg_isready PASS?"}
D --> F{"redis-cli ping PASS?"}
E -->|belum| C
F -->|belum| D
E -->|ya, healthy| G["Start api & worker"]
F -->|ya, healthy| G
G --> H["api buka :8080, worker konsumsi job"]Gambar 2. API dan worker baru start setelah PostgreSQL dan Redis lolos healthcheck, bukan sekadar container hidup.
Perintah kecil yang dijalankan Docker secara berkala untuk menilai apakah service di dalam container benar-benar sehat, bukan hanya proses containernya hidup. Hasilnya: healthy, unhealthy, atau starting.
Parameter healthcheck menentukan ketelitiannya. interval adalah jeda antar percobaan, timeout batas waktu satu percobaan, retries berapa kegagalan berturut sebelum dinyatakan unhealthy, dan start_period adalah masa warmup di mana kegagalan belum dihitung. Postgres kita beri start_period: 30s karena inisialisasi cluster pertama kali bisa lama.
Di banyak proyek Node/PHP lama, orang menempel script wait-for-it.sh di entrypoint untuk menunggu database. Compose healthcheck plus condition: service_healthy menggantikan hack itu secara deklaratif: kriteria “siap” ditulis sekali di YAML, bukan disebar di shell script tiap service.
Healthcheck Compose menolong saat development, tetapi production orchestration (ECS) tetap bisa restart, network hiccup, atau failover. Jaga tetap ada retry koneksi database di aplikasi Go saat membuka pgxpool, jangan bergantung penuh pada urutan start.
Untuk job sekali jalan seperti migration, ada kondisi lain yang berguna: service_completed_successfully. Service api bisa menunggu service migrate selesai dengan exit code 0 sebelum start, sehingga skema database selalu siap sebelum API menerima traffic. Berikut alur lengkap dari up sampai API siap, termasuk percobaan healthcheck yang berulang.
sequenceDiagram
participant CLI as docker compose
participant PG as postgres
participant API as api
CLI->>PG: start container
loop tiap interval sampai PASS
CLI->>PG: pg_isready -U ... -d ...
PG-->>CLI: belum siap (exit != 0)
end
PG-->>CLI: PASS (healthy)
CLI->>API: start container (depends_on terpenuhi)
API->>PG: open pgxpool, connect
API-->>CLI: listening :8080Gambar 3. Compose mengulang pg_isready tiap interval sampai PASS, baru kemudian menyalakan api yang membuka pool ke postgres.
Image distroless final tidak punya curl atau sh, jadi healthcheck api tidak bisa memakai curl localhost:8080. Definisikan healthcheck API sebagai subcommand binary (mis. ["/app", "healthcheck"]) atau cukup andalkan healthcheck di sisi Compose seperti contoh ini.
Environment File dan Konfigurasi
.env di Compose punya dua peran berbeda yang sering tertukar.
.env di Compose dipakai untuk dua hal: interpolasi placeholder di YAML, dan environment yang masuk ke dalam container. Membedakan keduanya menghilangkan setengah bug config pemula.
Compose otomatis membaca file bernama .env di direktori yang sama untuk mengganti placeholder seperti ${POSTGRES_USER} di dalam compose.yaml. Itu peran pertama: interpolasi. Tetapi mengganti placeholder di YAML tidak membuat variabel masuk ke proses di dalam container. Untuk itu butuh env_file: atau environment:. Di file kita, env_file: .env memasukkan seluruh variabel ke container, sedangkan blok environment: menimpa nilai yang memang harus berbeda di dalam network, seperti DATABASE_URL yang memakai host postgres, bukan localhost.
flowchart LR
DOTENV["File .env di root"] -->|peran 1: interpolasi| YAML["compose.yaml ${VAR}"]
DOTENV -->|peran 2: env_file| CTR["Env di dalam container"]
YAML -->|substitusi nilai| CTR
ENVBLOCK["blok environment:"] -->|override| CTR
CTR -->|os.Getenv| APP["Go: config.Load"]Gambar 4. Dua peran .env. Interpolasi mengisi placeholder YAML, env_file mengisi env di dalam container, lalu Go membacanya lewat config.Load.
.env.examplePOSTGRES_USER=skincare POSTGRES_PASSWORD=skincare_dev_password POSTGRES_DB=skincare_dev JWT_SECRET=dev_only_change_me PAYMENT_KEY=dev_payment_gateway_key
- GUI database (TablePlus, DBeaver, psql) terhubung lewat
localhost:5432. - Contoh:
postgres://skincare:skincare_dev_password@localhost:5432/skincare_dev?sslmode=disable.
- Service lain di network Compose mengakses PostgreSQL lewat hostname
postgres. - API memakai
postgres://skincare:skincare_dev_password@postgres:5432/skincare_dev?sslmode=disable.
Pola ini langsung menyambung ke modul secrets di Roadmap 7. Variabel .env lokal akan menjadi env var yang dibaca config.Load, sehingga satu image yang sama bisa dipromosikan dev, staging, lalu prod tanpa rebuild. Yang berubah hanya sumber env-nya: .env saat lokal, Secrets Manager saat di AWS.
Laravel membaca .env secara otomatis lewat framework. Compose tidak menyuntik env ke kode, ia menyuntik env ke container, lalu Go membacanya eksplisit di config.Load. Hasilnya rantai yang jelas: file .env jadi env var container jadi struct config yang divalidasi saat startup.
Commit .env.example, tetapi masukkan .env ke .gitignore dan .dockerignore. Secret development tetap secret. Kalau .env pernah masuk git, rotate nilainya karena history menyimpannya permanen.
Network, Port Mapping, dan Volume
Compose menarik garis tegas antara koneksi internal antar service dan akses dari host laptop.
Tiga konsep ini sering jadi sumber kebingungan: kapan pakai nama service, kapan pakai localhost, dan kapan data hilang. Compose punya jawaban yang konsisten untuk ketiganya.
Compose otomatis membuat satu network default untuk proyek. Di dalamnya, setiap service bisa memanggil service lain memakai nama service sebagai hostname: api ke postgres:5432, worker ke redis:6379. DNS internal Compose yang menerjemahkan nama itu ke IP container, jadi kamu tidak pernah perlu hardcode IP.
Port mapping membuka pintu dari host laptop ke container. Format ports: "8080:8080" berarti HOST:CONTAINER. Hanya api yang perlu di-publish karena hanya itu yang dipanggil dari browser, curl, Postman, atau frontend React. Worker tidak butuh port. Postgres juga tidak wajib di-publish, kecuali kamu ingin membukanya dengan GUI database dari host.
flowchart TB
subgraph host["Host laptop"]
BROWSER["Browser / curl / React dev"]
GUI["GUI DB (opsional)"]
end
subgraph net["Network Compose: skincare-backend_default"]
API["api"]
WK["worker"]
PG[("postgres")]
RD[("redis")]
end
BROWSER -->|localhost:8080| API
GUI -.->|localhost:5432 jika di-publish| PG
API -->|postgres:5432| PG
API -->|redis:6379| RD
WK -->|redis:6379| RD
PG -.->|named volume pgdata| VOL[("Disk host")]Gambar 5. Dari host pakai localhost dan port published, antar service pakai nama service. Data postgres tinggal di named volume.
Storage yang dikelola Docker dengan nama stabil, misalnya pgdata, sehingga data PostgreSQL tidak hilang hanya karena container dibuat ulang. Berbeda dari bind mount, lokasi fisiknya diurus Docker.
Named volume adalah alasan data tidak hilang. Container itu ephemeral: docker compose down lalu up membuat container baru dengan filesystem kosong. Tanpa volume, data Postgres ikut lenyap. Dengan pgdata:/var/lib/postgresql/data, direktori data Postgres dipetakan ke volume yang dikelola Docker, hidup terpisah dari siklus container.
Network
Network default membuat service saling resolve lewat nama (postgres, redis), bukan IP manual yang berubah tiap restart.
Port
8080:8080 (HOST:CONTAINER) hanya untuk development. Di production, database tidak pernah di-publish ke internet.
Volume
pgdata dan redisdata menyimpan state agar restart container tidak menghapus data lokal.
Jika kamu pernah memakai service discovery di Kubernetes atau pemanggilan service-to-service, nama service Compose adalah versi miniaturnya. http://postgres:5432 di Compose seperti memanggil service lewat nama internal, bukan IP. Pola berpikir ini terbawa langsung saat nanti deploy ke ECS dengan service discovery AWS.
Perbedaan dengan production penting untuk disadari sejak sekarang. Di Compose lokal, semua service berbagi satu network datar dan kita publish port database ke host demi kenyamanan. Di AWS nanti, API berjalan di ECS Fargate, database di RDS dalam private subnet, dan akses dibatasi security group, bukan port yang terbuka ke internet. Pola “nama service sebagai hostname” tetap relevan, tetapi siapa boleh menjangkau siapa menjadi jauh lebih ketat. Anggap Compose lokal sebagai latihan dengan pagar yang lebih longgar, dengan kebiasaan yang sudah benar.
Dari dalam container api, localhost menunjuk ke container api itu sendiri, bukan ke laptopmu. Inilah kesalahan nomor satu pendatang dari Node/PHP: menulis localhost:5432 di config padahal seharusnya postgres:5432.
Profil, Override, dan Hot Reload Dev
Satu base Compose bisa beradaptasi: aktifkan service opsional dengan profiles, sesuaikan dev lewat override file.
Sejauh ini kita punya satu compose.yaml. Tetapi kebutuhan dev sering berbeda dari baseline: kadang ingin menyalakan tool tambahan, kadang ingin hot reload tanpa rebuild image setiap edit. Compose menyediakan dua mekanisme rapi untuk ini: profiles dan override file.
Profiles membuat sebuah service hanya menyala saat profilnya diaktifkan. Cocok untuk dependency opsional seperti Adminer (GUI database berbasis web), Mailpit (penangkap email dev), atau seed job yang tidak perlu jalan setiap kali. Service tanpa profiles selalu menyala; service dengan profiles hanya menyala saat profil dipanggil lewat --profile.
compose.yaml (potongan profiles)services: adminer: image: adminer:5 ports: - "8081:8080" depends_on: postgres: condition: service_healthy profiles: ["tools"] mailpit: image: axllent/mailpit:latest ports: - "8025:8025" profiles: ["tools"]
Terminaldocker compose up -d docker compose --profile tools up -d
Penanda di service yang membuatnya hanya aktif saat profil terkait diaktifkan lewat --profile. Service tanpa profil selalu termasuk dalam stack default.
Mekanisme kedua adalah override file. Compose otomatis menggabungkan compose.yaml dengan compose.override.yaml (jika ada). Pola yang sehat: compose.yaml berisi baseline yang dipakai semua orang dan menyerupai production, sedangkan compose.override.yaml berisi penyesuaian khas dev yang tidak ingin kamu bawa ke staging, seperti bind mount source untuk hot reload.
compose.override.yamlservices: api: build: target: dev # stage builder dengan toolchain Go command: ["air", "-c", ".air.toml"] volumes: - .:/src # bind mount source untuk hot reload environment: APP_ENV: development
Di Node kamu pakai nodemon, di Laravel ada php artisan serve dengan auto-reload. Di Go padanannya adalah tool seperti air yang memantau file lalu rebuild dan restart binary. Override file membungkusnya rapi: produksi tetap pakai binary statis, dev pakai air dengan source di-mount.
Image production kita hanya berisi binary di base distroless tanpa toolchain Go. Hot reload butuh stage dengan source dan compiler (mis. target: dev ke stage builder). Jangan paksa air jalan di image distroless final, di sana tidak ada Go.
compose.yaml
Baseline yang menyerupai production: image final, command serve, tanpa source mount. Aman dibawa siapa saja.
compose.override.yaml
Penyesuaian dev: hot reload, bind mount, log verbose. Otomatis tergabung saat up, tidak perlu flag.
Menjaga compose.yaml semirip mungkin dengan production membuat surprise di deploy berkurang. Letakkan kenyamanan dev (hot reload, GUI tool) di override atau profiles, bukan di baseline.
Menjalankan Stack Lokal
Setelah Dockerfile Chapter 1 ada, workflow harian developer cukup beberapa perintah Compose.
Inti dari Compose adalah developer experience. Sekali file siap, menyalakan seluruh backend cukup tiga atau empat perintah yang sama di setiap laptop.
Salin .env.example menjadi .env, lalu isi credential development yang tidak pernah di-commit.
Jalankan docker compose up --build dari root agar image Go dibangun lalu seluruh dependency menyala dengan urutan yang benar.
Pakai docker compose ps untuk melihat status healthy, dan panggil endpoint health API dari host.
Pakai docker compose logs -f api untuk mengikuti log satu service secara realtime.
Jalankan docker compose down untuk menghentikan container tanpa menghapus named volume.
Terminalcp .env.example .env docker compose up --build -d docker compose ps
Terminalcurl http://localhost:8080/healthz docker compose logs -f api docker compose exec postgres pg_isready -U skincare -d skincare_dev
Untuk menjalankan migration sebelum API melayani traffic, jalankan sebagai perintah satu kali memakai image yang sama. Pola run --rm membuat container sementara yang otomatis terhapus setelah selesai.
Terminaldocker compose run --rm api /app migrate up
Terminaldocker compose down docker compose down -v
/healthz Smoke test lokal untuk memastikan container API sudah siap menerima request /v1/products Route katalog skincare yang bisa dites setelah migration dan seed data tersedia /v1/checkout Ubah keranjang jadi order, jalur yang melibatkan postgres dan worker docker compose down -v menghapus named volume, termasuk seluruh data PostgreSQL lokal. Berguna untuk reset total ke keadaan bersih, berbahaya jika kamu masih butuh datanya. Tanpa -v, data aman tersimpan.
docker compose up tanpa --build memakai image yang sudah ada, jauh lebih cepat. Tambahkan --build hanya ketika Dockerfile, go.mod, atau source code berubah dan kamu butuh image baru.
Jebakan Umum dari JS/PHP
Sebagian besar bug Compose lokal bukan bug Go, tetapi salah paham soal hostname, env, readiness, dan volume.
Hampir semua kegagalan stack pemula berasal dari empat salah paham yang sama. Mengenalinya sekali menghemat berjam-jam debugging.
localhost di container
Dari container API, localhost adalah container itu sendiri. Untuk database, pakai hostname postgres, bukan localhost.
depends_on bukan migrasi
Healthcheck memastikan database siap, tetapi tidak menjalankan migration. Migration perlu command tersendiri atau service dengan service_completed_successfully.
.env tidak selalu masuk container
.env otomatis dipakai untuk interpolasi YAML, tetapi container butuh env_file atau environment agar benar-benar menerima variabel.
Volume menyimpan bug lama
Skema database lama bisa tetap ada walau image berubah. Reset volume hanya saat memang ingin menghapus state lokal.
Lupa $$ di healthcheck
$POSTGRES_USER diinterpolasi Compose menjadi kosong. Tulis $${POSTGRES_USER} agar diteruskan ke shell container.
Menempel version: di file
Compose v2 modern menganggap version deprecated. Hapus barisnya agar tidak ada warning di tiap up.
Stack Laravel sering memisahkan Nginx, PHP-FPM, queue worker, scheduler, dan database menjadi banyak service. Go API umumnya satu binary HTTP, worker satu binary atau subcommand lain. Karena tidak ada runtime interpreter dan node_modules untuk dijalankan, Compose file Go jauh lebih pendek dan startup-nya nyaris instan.
- Butuh runtime di image final (Node ~150MB+, PHP-FPM ~100MB+).
node_modulesatauvendor/ikut, atau di-install saat build.- Sering perlu
tiniatau process manager agar sinyal SIGTERM tertangani.
- Satu binary statis di base ~2MB (distroless), tanpa runtime.
- Tidak ada
go mod downloadsaat runtime, semua sudah ter-compile. - Binary jadi PID 1 dan menerima SIGTERM langsung untuk graceful shutdown.
Di host pakai localhost. Antar container pakai nama service. Secret lokal simpan di .env. State database simpan di named volume. Empat aturan ini menutup mayoritas bug Compose pemula.
Saat stack berperilaku aneh, beberapa perintah ini hampir selalu cukup untuk menemukan akar masalahnya tanpa menebak.
Terminaldocker compose config docker compose ps docker compose logs --tail=50 api docker compose exec api env | grep DATABASE_URL docker compose exec api ping -c1 postgres
docker compose config mencetak file hasil merge dan interpolasi yang sebenarnya dipakai, sehingga kamu bisa memverifikasi ${POSTGRES_USER} benar-benar terisi. exec api env membuktikan variabel yang masuk ke container, dan exec api ping postgres membuktikan nama service teresolusi di network. Jika ping gagal, masalahnya jaringan; jika env kosong, masalahnya konfigurasi env.
Sebelum mengubah YAML berdasarkan dugaan, jalankan docker compose config. Ia menampilkan persis bagaimana Compose menafsirkan file setelah interpolasi dan merge override, sering langsung menunjukkan placeholder yang tidak terisi.
Ringkasan & Poin Penting
Docker Compose mengubah local development dari kumpulan setup manual yang berbeda di tiap laptop menjadi satu stack yang bisa dijalankan ulang persis sama oleh seluruh tim.
Yang Wajib Menempel
- Compose mendefinisikan
api,worker,postgres, danredisdalam satucompose.yamldi root proyek, tanpa keyversionyang sudah deprecated. depends_ondengancondition: service_healthymembuat API dan worker menunggu PostgreSQL dan Redis benar-benar siap menerima koneksi, bukan sekadar hidup.- Healthcheck
pg_isreadydanredis-cli pingplusstart_periodyang cukup mencegah API crash saat boot karena database belum warmup. .envpunya dua peran: interpolasi placeholder YAML, dan, lewatenv_file, mengisi env di dalam container yang dibacaconfig.Load.- Dari host pakai
localhost, antar container pakai nama service (postgres,redis);localhostdi container menunjuk ke container itu sendiri. - Named volume
pgdatamenjaga data PostgreSQL tetap ada setelah container dihentikan;down -vmenghapusnya. - Port
8080:8080nyaman untuk development, tetapi port database tidak pernah dibuka begitu saja ke internet di production. $$meng-escape$agar diteruskan ke shell container, bukan diinterpolasi Compose.
Untuk proyek online shop skincare, stack ini menjadi pondasi menjalankan API katalog, checkout, payment webhook, worker email, cache Redis, dan integration test lokal dengan cara yang sama di semua mesin. Image Go yang dibangun di sini identik dengan yang akan dipromosikan ke production, sehingga “jalan di laptopku” benar-benar berarti “akan jalan di AWS”.
Chapter 1 membuat image Go yang kecil dan aman. Chapter 2 ini menjalankan image itu bersama dependency lokal. Chapter 3 membawa pola yang sama ke CI pipeline (lint, format, unit test, integration test, build, push image) agar build dan test tidak lagi bergantung pada laptop developer, lalu Chapter 4 dan seterusnya men-deploy image itu ke ECS Fargate dengan RDS PostgreSQL.
Progress disimpan lokal di browser ini.