Kita baru saja menutup Section Jaringan Container dengan perangkat yang lengkap: mulai dari network namespace dan bridge, DNS internal, sampai resep diagnosis ketika koneksi antar container macet. Dengan bekal itu, tidak ada stack multi-container yang tidak bisa kita rakit dengan tangan — buat network, nyalakan database di dalamnya, terbitkan port untuk aplikasi, lalu hubungkan semuanya. Justru di titik inilah masalahnya bergeser: yang melelahkan bukan lagi bisa atau tidaknya merakit stack, melainkan jumlah perintah yang harus diulang setiap kali stack itu hendak dinyalakan di mesin baru. Section ini memperkenalkan Docker Compose, alat yang mengubah seluruh deretan perintah manual tersebut menjadi satu berkas YAML yang bisa di-commit ke repositori, dibaca oleh rekan tim, dan dijalankan ulang dengan satu perintah tunggal.

Deretan Perintah yang Panjang dan Rapuh #

Budi memulai proyek baru bernama katalog — aplikasi web sederhana yang menyimpan daftar produk di PostgreSQL. Seluruh kebutuhannya sudah bisa dipenuhi dengan perintah-perintah yang kita kuasai dari section sebelumnya. Setiap kali Budi ingin menyalakan stack dari nol, inilah yang harus diketiknya:

bash
$ docker network create net-katalog
$ docker run -d --name db-katalog \
    --network net-katalog \
    -e POSTGRES_USER=katalog \
    -e POSTGRES_PASSWORD=rahasia123 \
    -e POSTGRES_DB=katalog \
    postgres:18-alpine
$ docker run -d --name web-katalog \
    --network net-katalog \
    -p 8080:80 \
    nginx:1.29-alpine

Dua belas baris berisi flag, nama, dan nilai environment itu berhasil, dan setiap perintahnya meninggalkan jejak panjang di terminal.

Output yang Dihasilkan:

text
9e4c7b2a1f8d3c6a9b4e2f7d1c5a8e3b6f9d2c7a4e1b8f3d5c6a2e9b7d4f1a3c
c7a2e9b4f1d8c3a6e5b2f7d9c4a1e8b3f6d2a7c9e4b1f5d8a3c6e2b7f4a1d9c5e
3f8b1d6a9c2e7f4b3d1a8c5e2f7b4d9a6c1e3f8b2d7a4c9e6f1b3d5a8c2e7f4b

Baris pertama adalah ID network yang baru tercipta, dua baris berikutnya ID kedua container — deretan panjang yang tidak kita baca lagi setelahnya. Perintah-perintah itu memang berhasil, tetapi berhasil dengan syarat yang banyak. Mari kita bedah empat titik nyerinya satu per satu:

  1. Panjang. Dua belas baris berisi flag, nama, dan nilai environment sebelum stack menyala. Satu salah ketik pada POSTGRES_PASSWORD membuat database lahir dengan kredensial yang berbeda dari yang aplikasi harapkan, dan galatnya baru terasa beberapa menit kemudian saat koneksi ditolak.
  2. Urutan. Network harus lahir lebih dulu sebelum dua docker run bisa bergabung ke dalamnya; database sebaiknya menyala sebelum aplikasi mencoba terhubung. Menukar urutan tidak selalu memicu galat langsung — kadang hanya menghasilkan kegagalan koneksi yang baru terasa dari dalam aplikasi, persis jenis masalah yang kita latih diagnosisnya pada Chapter Troubleshooting Network.
  3. Dokumentasi. Setelah stack menyala, konfigurasi lengkapnya hanya hidup di riwayat shell dan ingatan Budi. Tidak ada satu berkas di proyek yang bisa dibaca ulang besok pagi untuk menjawab “port berapa saja yang diterbitkan? variabel apa saja yang dipasang pada db?”.
  4. Rekan tim. Siti ingin menjalankan katalog yang sama di mesinnya — dan satu-satunya jalan adalah menyalin-tempel deretan perintah itu dari obrolan, lalu berdoa flag-flagnya tidak terpotong. Padahal kisah Budi dan Siti pada Chapter Use Case Docker sudah menjanjikan masa depan yang lebih baik: satu berkas di repositori, satu perintah di mesin siapa pun.

Compose: Deklarasi dalam Satu Berkas YAML #

Docker Compose menjawab keempat nyeri itu dengan satu perubahan cara pandang: alih-alih mengeksekusi langkah demi langkah, kita mendeklarasikan kondisi akhir yang diinginkan di dalam satu berkas YAML, lalu meminta Compose mewujudkannya. Perintahnya ditulis docker compose — dua kata dengan spasi — karena Compose hadir sebagai plugin CLI yang menyatu pada perintah docker; kita sudah melihatnya tercantum sebagai compose* pada daftar Management Commands di Chapter docker help dan docker version, dan terpasang lewat paket plugin pada Chapter Instalasi Docker di Linux.

Perbedaan cara kerjanya terbaca paling jelas sebagai alur. Pendekatan manual mengeksekusi perintah satu per satu dan berhenti di situ; pendekatan deklaratif membaca berkas, membandingkannya dengan keadaan daemon, lalu mewujudkan selisihnya:

Mari kita telaah diagram di atas: cabang kiri adalah perilaku yang akan kita saksikan pada up berulang — container yang sudah cocok dengan deklarasi tidak disentuh; cabang kanan adalah yang terjadi ketika berkas berubah — Compose menciptakan ulang container yang berbeda. Kedua cabang berujung pada kondisi yang sama, dan itulah janji deklaratif: berkas selalu menang.

Berkas deklarasi itu punya nama resmi. Compose mencari empat nama berikut secara berurutan, lalu memakai yang pertama ditemukan:

PrioritasNama berkasStatus
1compose.yamlnama resmi — pilihan utama untuk berkas baru
2compose.ymlalternatif sah bila konvensi tim memakai .yml
3docker-compose.yamlnama warisan era Compose v1 — masih dikenali
4docker-compose.ymlnama warisan era Compose v1 — masih dikenali

Dua nama warisan pada prioritas tiga dan empat memakai tanda hubung di antara kata docker dan compose — bentuk yang diwariskan dari generasi pertama Compose. Proyek lama yang kita temui di lapangan sering masih memakai nama tersebut, tetapi seluruh Course ini menulis compose.yaml tanpa perkecualian, mengikuti konvensi resmi modern.

Sejarah singkatnya pantas dikenali karena menjelaskan mengapa dua kebiasaan lama masih berkeliaran di dokumentasi internet:

PeriodePeristiwa
2013Orchard memperkenalkan fig, tool orchestrator multi-container berbasis YAML yang pertama populer
2014Docker, Inc. mengakuisisi Orchard; fig dibesarkan menjadi Docker Compose
2014–2021Compose v1 — program Python terpisah dengan nama perintahnya sendiri
2021Compose v2 — ditulis ulang dalam Go dan lahir sebagai plugin docker compose
2026Versi yang terpasang di Course ini

Versi yang berjalan di mesin kita bisa diperiksa dengan satu perintah:

bash
$ docker compose version

Output yang Dihasilkan:

text
Docker Compose version v5.6.0

Angka v5.6.0 itu identik dengan yang tercantum pada daftar Plugins di output docker version — satu program yang sama, dipanggil dari dua pintu. Sepanjang section ini kita memakai bentuk plugin docker compose secara konsisten; bentuk lama dengan tanda hubung sudah berstatus legacy dan tidak lagi menerima fitur baru.

compose.yaml Pertama untuk Proyek katalog #

Saatnya mewujudkan janji Chapter Use Case Docker. Di dalam folder proyek katalog, Budi membuat berkas compose.yaml yang mendeklarasikan kedua service — web dan db — dalam bentuk paling minimal yang bisa jalan:

yaml
services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "8080:80"

  db:
    image: postgres:18-alpine
    environment:
      - POSTGRES_PASSWORD=rahasia123

Mari kita telaah isi berkas ini baris demi baris:

  1. services: — satu-satunya blok yang wajib ada; seluruh container yang kita inginkan dideklarasikan di bawahnya sebagai pemetaan nama-ke-konfigurasi. Nama web dan db bebas kita pilih dan akan menjadi identitas resmi kedua service di sepanjang perintah Compose.
  2. image: nginx:1.29-alpine — service web memakai image siap pakai persis yang kita kenal dari Section Docker Image; untuk sekarang nginx berperan sebagai pengganti sementara aplikasi katalog yang sesungguhnya, dan akan kita ganti dengan image hasil build sendiri pada Chapter Image, Build, dan Pull.
  3. ports: dengan "8080:80" — padanan langsung flag -p 8080:80: port host 8080 diterbitkan menuju port 80 di dalam container, lengkap dengan tanda kutip yang maknanya kita bedah pada Chapter Struktur compose.yaml.
  4. environment: dengan - POSTGRES_PASSWORD=rahasia123 — padanan flag -e dari Chapter Environment Variable, ditulis sebagai daftar; tanpa variabel ini container postgres menolak menyala, persis peringatan yang sudah kita saksikan pada Chapter Use Case Docker.
  5. Tidak ada version: dan tidak ada networks: — kunci version: sudah obsolete dan sengaja tidak ditulis; blok network juga tidak kita deklarasikan karena Compose otomatis membuat satu network bersama untuk seluruh service dalam berkas, pengetahuan yang dasarnya kita pelajari pada Section Jaringan Container dan yang akan kita bedah tuntas pada Chapter Port dan Network.

Sepuluh baris YAML di atas menggantikan seluruh deretan docker network create dan dua docker run — dan ia tersimpan sebagai berkas biasa yang bisa di-commit.

Satu Perintah untuk Menyalakan Seluruh Stack #

Budi meng-commit compose.yaml ke repositori katalog dan mendorongnya. Di mesinnya sendiri ia tinggal menjalankan satu perintah dari dalam folder proyek; output berikut diambil dari laptop Siti yang baru menarik repositori yang sama — laptopnya belum pernah menarik kedua image, sehingga proses penarikannya ikut terlihat:

bash
$ docker compose up

Output yang Dihasilkan:

text
[+] Pulling 2/2
 ✔ db Pulled                                                          7.2s
 ✔ web Pulled                                                         4.9s
[+] Running 3/3
 ✔ Network katalog_default Created                                    0.2s
 ✔ Container katalog-db-1 Created                                     0.1s
 ✔ Container katalog-web-1 Created                                    0.0s
 ✔ Container katalog-db-1 Started                                     0.4s
 ✔ Container katalog-web-1 Started                                    0.3s
Attaching to katalog-db-1, katalog-web-1
katalog-db-1   | The files belonging to this database system will be owned by user "postgres".
katalog-db-1   | This user must also own the server process.
katalog-db-1   |
katalog-db-1   | initdb: warning: enabling "trust" authentication for local connections only.
...
katalog-db-1   | 2026-10-13 16:12:08.447 UTC [1] LOG:  starting PostgreSQL 18.4 on x86_64-pc-linux-musl, compiled by gcc (Alpine 14.2.0) 14.2.0, 64-bit
katalog-db-1   | 2026-10-13 16:12:08.612 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
katalog-db-1   | 2026-10-13 16:12:08.731 UTC [1] LOG:  database system is ready to accept connections
katalog-web-1  | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to process configuration files
katalog-web-1  | /docker-entrypoint.sh: Configuration complete; ready for start up
katalog-web-1  | 2026/10/13 16:12:09 [notice] 1#1: using the "epoll" event method
katalog-web-1  | 2026/10/13 16:12:09 [notice] 1#1: start worker processes
katalog-web-1  | 2026/10/13 16:12:09 [notice] 1#1: start worker process 39
...

Baris-baris inisialisasi initdb di bagian tengah dipotong agar ringkas. Mari kita telaah apa yang Compose kerjakan dari output di atas:

  1. [+] Pulling 2/2 — Compose memeriksa kedua image yang dideklarasikan; di laptop Siti keduanya belum ada, sehingga Compose menariknya lebih dulu persis seperti docker pull, tanpa satu perintah tambahan dari pengguna.
  2. ✔ Network katalog_default Created — network dibuat otomatis dan dinamai mengikuti nama project; nama project sendiri diambil dari nama folder, yaitu katalog, sehingga network-nya bernama katalog_default. Inilah padanan docker network create net-katalog yang dulu Budi ketik manual.
  3. katalog-db-1 dan katalog-web-1 — container dinamai dengan pola <project>-<service>-<index>; tidak ada satu pun flag --name yang kita tulis, dan pola penamaan ini akan kita bedah perannya pada Chapter Service.
  4. Attaching to ... dan log gabungan — karena up dijalankan tanpa -d, Compose menempelkan terminal ke log kedua container sekaligus; setiap baris diawali nama container pengirimnya, sehingga riwayat inisialisasi postgres dan startup nginx terbaca berdampingan dalam satu tampilan.
  5. Tekan Ctrl+C untuk melepas tampilan log dan menghentikan kedua container — Compose mengirim sinyal berhenti kepada keduanya secara tertib, dan nama stack katalog tetap tercatat sehingga bisa dinyalakan lagi kapan pun.

Pembongkarannya sama singkatnya. Dari terminal yang sama, Siti menjalankan:

bash
$ docker compose down

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Container katalog-web-1 Removed                                    0.4s
 ✔ Container katalog-db-1 Removed                                     0.5s
 ✔ Network katalog_default Removed                                    0.2s

Mari kita telaah hasil down tersebut: kedua container dihapus dari daemon, lalu network bawaan project ikut dibongkar — tiga objek yang diciptakan up dibersihkan utuh oleh satu perintah, dan menjalankan up kembali besok pagi akan menciptakan ketiganya dari nol dengan konfigurasi yang sama persis karena sumber kebenarannya adalah berkas, bukan ingatan.

Kilas ke depan sebelum kita melangkah: sepanjang section ini, deretan tanda ✔ pada output up akan semakin kaya. Begitu service db memasang healthcheck dan web menyatakan dirinya menunggu, output stack katalog akan tumbuh menjadi seperti ini:

text
[+] Running 3/3
 ✔ Network katalog_default Created
 ✔ Container katalog-db-1 Healthy
 ✔ Container katalog-web-1 Started

Baris ✔ Container katalog-db-1 Healthy adalah gerbang kesehatan yang kita bangun pada Chapter Healthcheck dan Service Readiness — persis perilaku “menunggu database sehat, baru menyalakan aplikasi” yang diceritakan Chapter Use Case Docker. Seluruh fitur yang menanti di depan bisa dipetakan begitu saja:

FiturKunci di compose.yamlDibahas di
Service dan containerservicesService
Image siap pakai dan build sendiriimage, buildImage, Build, dan Pull
Port dan network antar serviceports, expose, networksPort dan Network
Persistensi datavolumesVolume dan Bind Mount
Environment variableenvironment, env_fileEnvironment dan env File
Urutan start antar servicedepends_ondepends_on dan Startup Order
Kesehatan servicehealthcheckHealthcheck dan Service Readiness
Service opsional per skenarioprofilesProfiles
Sinkronisasi kode saat developdevelop.watchCompose Watch

Berkas sepuluh baris tadi adalah pintu masuknya, dan setiap baris pada tabel di atas akan menambah satu kemampuan pada berkas yang sama. Sebelum menumbuhkannya, kita perlu membaca peta anatomi berkas itu dengan saksama: blok apa saja yang sah, bagaimana aturan indentasinya, dan bagaimana memvalidasinya tanpa menjalankan satu container pun. Chapter Struktur compose.yaml membedah peta tersebut.

Referensi #