Web Artisan
Beranda

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.

Roadmap 8 · Docker, CI/CD, dan AWS Deployment

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.

Compose Spec 2026Go 1.26~65 menit baca
01

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.

🎼Analogi: partitur orkestra

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.

🌉Jembatan: mirip Laravel Sail, tetapi lebih vanilla

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.

Laravel Sail
  • 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.
Docker Compose untuk Go
  • Kita menulis compose.yaml langsung di root proyek.
  • Service api dan worker memakai image Go dari Dockerfile Chapter 1.
  • Kita menentukan sendiri readiness, env, volume, network, dan port.
Docker Compose

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.

02

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.

⚠️Hapus baris version dari tutorial lama

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
🌉Jembatan: dari package.json scripts ke Compose

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.

🌉Jembatan: command Compose vs ENTRYPOINT Dockerfile

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.

Compose Spec

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.

03

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.

Posisi compose.yaml di proyek skincare
  • 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
📝Satu Dockerfile, dua binary

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.

⚠️Pastikan .dockerignore menolak .env

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.

04

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.go
package 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) } }
service

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.

🌉Jembatan: dari React dev server ke backend stack

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.

05

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.yaml
name: 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:
⚠️command harus cocok dengan binary

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.

📝Tentang tag image

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

💡Aturan dolar ganda

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.

06

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.

healthcheck

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.

🌉Jembatan: bukan sekadar wait-for-it.sh

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.

💡Tetap retry koneksi di aplikasi

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

Gambar 3. Compose mengulang pg_isready tiap interval sampai PASS, baru kemudian menyalakan api yang membuka pool ke postgres.

⚠️Distroless tidak punya shell untuk healthcheck

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.

07

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.example
POSTGRES_USER=skincare POSTGRES_PASSWORD=skincare_dev_password POSTGRES_DB=skincare_dev JWT_SECRET=dev_only_change_me PAYMENT_KEY=dev_payment_gateway_key
Dari host laptop
  • GUI database (TablePlus, DBeaver, psql) terhubung lewat localhost:5432.
  • Contoh: postgres://skincare:skincare_dev_password@localhost:5432/skincare_dev?sslmode=disable.
Dari dalam container
  • 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.

🌉Jembatan: dari env() Laravel ke env_file Compose

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.

⚠️Jangan commit .env

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.

08

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.

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.

🌉Jembatan: nama service mirip service discovery

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.

⚠️localhost di dalam container bukan host laptop

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.

09

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"]
Terminal
docker compose up -d docker compose --profile tools up -d
profile

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.yaml
services: 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
🌉Jembatan: hot reload seperti nodemon / php artisan serve

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.

⚠️Hot reload butuh source di image, bukan binary statis

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.

💡Pisahkan dev dari production sejak file

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.

10

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.

Siapkan .env lokal

Salin .env.example menjadi .env, lalu isi credential development yang tidak pernah di-commit.

Build dan start stack

Jalankan docker compose up --build dari root agar image Go dibangun lalu seluruh dependency menyala dengan urutan yang benar.

Cek kesehatan stack

Pakai docker compose ps untuk melihat status healthy, dan panggil endpoint health API dari host.

Lihat log saat debugging

Pakai docker compose logs -f api untuk mengikuti log satu service secara realtime.

Matikan stack

Jalankan docker compose down untuk menghentikan container tanpa menghapus named volume.

Terminal
cp .env.example .env docker compose up --build -d docker compose ps
Terminal
curl 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.

Terminal
docker compose run --rm api /app migrate up
Terminal
docker compose down docker compose down -v
GET /healthz Smoke test lokal untuk memastikan container API sudah siap menerima request
GET /v1/products Route katalog skincare yang bisa dites setelah migration dan seed data tersedia
POST /v1/checkout Ubah keranjang jadi order, jalur yang melibatkan postgres dan worker
⚠️Hati-hati dengan down -v

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.

💡Rebuild hanya saat perlu

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.

11

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.

🌉Jembatan: dari PHP-FPM + Nginx ke satu binary Go

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.

Image Node / PHP
  • Butuh runtime di image final (Node ~150MB+, PHP-FPM ~100MB+).
  • node_modules atau vendor/ ikut, atau di-install saat build.
  • Sering perlu tini atau process manager agar sinyal SIGTERM tertangani.
Image Go
  • Satu binary statis di base ~2MB (distroless), tanpa runtime.
  • Tidak ada go mod download saat runtime, semua sudah ter-compile.
  • Binary jadi PID 1 dan menerima SIGTERM langsung untuk graceful shutdown.
💡Rule of thumb yang menempel

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.

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

🔎config dulu sebelum menebak

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.

12

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, dan redis dalam satu compose.yaml di root proyek, tanpa key version yang sudah deprecated.
  • depends_on dengan condition: service_healthy membuat API dan worker menunggu PostgreSQL dan Redis benar-benar siap menerima koneksi, bukan sekadar hidup.
  • Healthcheck pg_isready dan redis-cli ping plus start_period yang cukup mencegah API crash saat boot karena database belum warmup.
  • .env punya dua peran: interpolasi placeholder YAML, dan, lewat env_file, mengisi env di dalam container yang dibaca config.Load.
  • Dari host pakai localhost, antar container pakai nama service (postgres, redis); localhost di container menunjuk ke container itu sendiri.
  • Named volume pgdata menjaga data PostgreSQL tetap ada setelah container dihentikan; down -v menghapusnya.
  • Port 8080:8080 nyaman 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”.

🧭Peta ke roadmap

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.