Peta anatomi pada chapter sebelumnya menempatkan services sebagai satu-satunya blok wajib — dan memang di sanalah seluruh kehidupan stack berlangsung. Dua baris web dan db pada berkas katalog kita sejauh ini masih memakai dua kunci saja, padahal sebuah service sanggup menerima delapan belas kunci konfigurasi yang paling sering dipakai sehari-hari. Chapter ini membedah konsep service itu sendiri — mengapa ia disebut cetakbiru dan bukan container, bagaimana pola penamaan <project>-<service>-<index> bekerja — lalu menumbuhkan berkas katalog dengan kebijakan restart dan trio variabel PostgreSQL, memperkenalkan docker compose ps beserta kolom khasnya, dan menutup dengan dua alat identitas: container_name untuk nama tetap serta --scale untuk banyak replika sekaligus.

Service: Cetakbiru yang Diinstansiasi #

Kata service pada Compose punya makna yang tepat dan sering disalahpahami. Sebuah service bukanlah container: service adalah cetakbiru — deklarasi lengkap tentang bagaimana container miliknya harus diciptakan, dari image apa, dengan environment apa, dan port mana yang diterbitkan. Container yang berjalan di daemon adalah instansiasi dari cetakbiru itu, hubungan yang persis seperti hubungan kelas dengan objek pada pemrograman berorientasi objek.

Bukti hubungan itu terlihat langsung pada nama container. Ketika Compose menciptakan container untuk service web pada project katalog, container tersebut bernama katalog-web-1 — pola <project>-<service>-<index> — tanpa satu pun flag --name yang kita tulis pada berkas. Angka 1 di ujung bukan hiasan: ia adalah slot replika yang akan terisi 2, 3, dan seterusnya ketika kita menskalakan service di akhir chapter ini. Nama container yang dulu kita tentukan manual lewat --name kini dihasilkan dari tiga identitas yang sudah kita pahami: project dari blok name, service dari kunci di bawah services, dan index dari jumlah replika.

Karena container hanyalah instansiasi, menghapusnya tidak pernah mengubah cetakbiru. Perintah docker compose up selalu membaca berkas, membandingkan deklarasi dengan keadaan daemon, lalu menciptakan ulang container yang diperlukan — keandalan yang akan kita saksikan berulang kali sepanjang section ini.

Delapan Belas Kunci yang Paling Sering Dipakai #

Sebelum menumbuhkan berkas katalog, mari kita petakan dulu seluruh kunci yang akan kita jumpai berulang kali. Tabel berikut merangkum delapan belas kunci konfigurasi service yang paling sering dipakai beserta chapter yang membahasnya tuntas:

KunciPeranDibahas di
imageimage siap pakai dari registryImage, Build, dan Pull
buildmembangun image sendiri dari DockerfileImage, Build, dan Pull
container_namenama tetap pengganti pola otomatischapter ini
commandmenimpa CMD imageCommand Override
entrypointmenimpa ENTRYPOINT imageCommand Override
environmentvariabel lingkungan untuk serviceEnvironment dan env File
env_filememuat variabel dari berkas terpisahEnvironment dan env File
portsmenerbitkan port ke hostPort dan Network
exposemendokumentasikan port antar servicePort dan Network
volumesmenempel volume atau bind mountVolume dan Bind Mount
networksnetwork yang dimasuki servicePort dan Network
depends_onurutan dan syarat start antar servicedepends_on dan Startup Order
restartkebijakan bangkit otomatisRestart Policy
working_dirdirektori kerja proses containerWorking Directory
useridentitas UID/GID prosesUser dan Permissions
healthcheckperintah pemeriksaan kesehatanHealthcheck dan Service Readiness
labelslabel metadata pada containerVOLUME dan LABEL
deploybatas resource dan replika lanjutanResource di Compose

Sebagian besar baris tabel itu adalah padanan langsung flag docker run yang sudah kita kuasai dari Section Konfigurasi Container — environment menggantikan -e, working_dir menggantikan -w, user menggantikan --user. Perbedaan besarnya satu: seluruh nilai kini hidup di berkas yang bisa dibaca, dibedah, dan di-commit, bukan di riwayat shell.

Berkas katalog kita tumbuh dua langkah pada chapter ini. Service db menerima trio variabel yang dulu kita buktikan efeknya lewat container db-katalog pada Chapter Environment Variable, dan kedua service menerima kebijakan restart:

yaml
name: katalog

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "8080:80"
    restart: unless-stopped

  db:
    image: postgres:18-alpine
    environment:
      - POSTGRES_USER=katalog
      - POSTGRES_PASSWORD=rahasia123
      - POSTGRES_DB=katalog
    restart: unless-stopped

Mari kita telaah pertumbuhan tersebut:

  1. Trio POSTGRES_USER, POSTGRES_PASSWORD, dan POSTGRES_DB — ketiganya dibaca entrypoint postgres saat direktori data masih kosong: superuser bernama katalog dengan kata sandi rahasia123 dan basis data awal bernama katalog; nilai yang sama persis dengan demo db-katalog pada Chapter Environment Variable, kini tertulis sebagai deklarasi dan bukan flag yang hilang di riwayat shell.
  2. restart: unless-stopped pada kedua service — padanan flag --restart dari Chapter Restart Policy: container yang mati mendadak dibangkitkan lagi, container yang kita hentikan manual tetap berhenti, dan keduanya ikut hidup kembali saat daemon menyala setelah server reboot.
  3. Kebijakan menempel per service — restart ditulis pada web dan db secara terpisah karena keduanya berhak memilih kebijakan berbeda; tidak ada satu kunci yang berlaku untuk seluruh berkas kecuali kita memakai x- fragment dari chapter sebelumnya.
  4. Bentuk daftar pada environment — kita tetap memakai bentuk daftar - KUNCI=nilai karena mudah dibaca berurutan; renderer docker compose config akan menyeragamkannya menjadi pemetaan, persis bukti pada Chapter Struktur compose.yaml.

Menjalankan dalam Latar dan Membaca ps #

Berkas yang lebih kaya ini kita nyalakan dalam mode latar dengan flag -d — padanan -d pada docker run. Sejak menghapusnya di Chapter Menghapus Image, image nginx:1.29-alpine tidak lagi tersimpan di mesin Budi, sehingga Compose menariknya kembali lebih dulu:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Pulling 1/1
 ✔ web Pulled                                                         4.9s
[+] Running 5/5
 ✔ Network katalog_default Created                                    0.2s
 ✔ Container katalog-db-1 Created                                     0.1s
 ✔ Container katalog-db-1 Started                                     0.5s
 ✔ Container katalog-web-1 Created                                    0.0s
 ✔ Container katalog-web-1 Started                                    0.4s

Inventaris stack kini dibaca lewat perintah milik Compose sendiri:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME             IMAGE               COMMAND                  SERVICE   CREATED              STATUS              PORTS
katalog-db-1     postgres:18-alpine  "docker-entrypoint.s…"   db        About a minute ago   Up About a minute   5432/tcp
katalog-web-1    nginx:1.29-alpine   "/docker-entrypoint.…"   web       About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Mari kita telaah isi tabel tersebut:

  1. Kolom SERVICE — kolom yang tidak dimiliki docker ps bawaan; kolom inilah tanda tangan Compose. Satu image yang sama boleh dipakai banyak service sekaligus, sehingga identitas container yang sesungguhnya penting bukan imagenya, melainkan service cetakbirunya — dan saat kita menskalakan service di bawah, kolom inilah yang membedakan asal tiga container kembar.
  2. NAME mengikuti pola <project>-<service>-<index> — katalog-db-1 dan katalog-web-1, persis pola yang kita bedah di awal chapter.
  3. PORTS untuk db hanya mencantumkan 5432/tcp — tanpa pemetaan ke host, karena service db tidak punya kunci ports; angka itu sekadar metadata EXPOSE milik image postgres yang kita kenal dari Chapter EXPOSE dan Port.
  4. COMMAND menampilkan perintah bawaan image dalam bentuk terpotong — entrypoint postgres dan nginx yang sudah kita bedah pada Chapter CMD dan ENTRYPOINT.

Compose juga bisa memandang lebih tinggi: menampilkan daftar seluruh project yang sedang berjalan di daemon, apa pun foldernya:

bash
$ docker compose ls

Output yang Dihasilkan:

text
NAME      STATUS        FILES
katalog   running(2)    /home/budi/katalog/compose.yaml

Dua kolom terakhirnya langsung menjelaskan diri: status running(2) berarti dua container hidup di bawah project itu, dan FILES menunjuk berkas deklarasi yang menjadi sumber kebenarannya.

Project Kedua dengan -p #

Chapter sebelumnya menjanjikan peragaan flag -p, dan skenarionya nyata: Budi ingin menjalankan stack yang sama berdampingan sebagai lingkungan uji. Perintah berikut ia coba dari folder katalog tempat project pertama masih menyala:

bash
$ docker compose -p katalog-uji up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog-uji_default Created                                0.2s
 ✔ Container katalog-uji-db-1 Started                                 0.5s
 ✔ Container katalog-uji-web-1 Created                                0.1s
Error response from daemon: driver failed programming external connectivity on endpoint katalog-uji-web-1 (9a3f2c8e4b1d): Bind for 0.0.0.0:8080 failed: port is already allocated

Mari kita telaah kegagalan yang setengah jalan itu:

  1. Flag -p katalog-uji berhasil mengganti identitas project — network katalog-uji_default dan container katalog-uji-db-1 lahir dengan prefix baru, bukti bahwa nama project memang bisa dipaksakan perintah tanpa menyentuh berkas.
  2. db sempat menyala sepenuhnya — service db tidak menerbitkan port host apa pun, sehingga tidak ada yang bisa konflik; kegagalan hanya menimpa web.
  3. Galatnya adalah konflik port lama kita — Bind for 0.0.0.0:8080 failed: port is already allocated, persis wujud yang kita bedah pada Chapter Port Publishing; pemegang port 8080 adalah katalog-web-1 milik project pertama, dan satu pintu host tidak boleh dipakai dua pemetaan sekaligus.
  4. Satu berkas memang hanya punya satu nilai ports — selama kedua project membaca berkas yang sama, keduanya menuntut port yang sama; lingkungan kedua menuntut berkas dengan nomor port berbeda.

Budi membersihkan sisa kegagalan itu, lalu menyiapkan berkas uji sebagai salinan dengan satu angka yang digeser:

bash
$ docker compose -p katalog-uji down
$ mkdir -p ../katalog-uji
$ cp compose.yaml ../katalog-uji/compose.yaml
$ sed -i 's/"8080:80"/"8081:80"/' ../katalog-uji/compose.yaml
$ cd ../katalog-uji
$ docker compose -p katalog-uji up -d

Output yang Dihasilkan:

text
[+] Running 5/5
 ✔ Network katalog-uji_default Created                                0.2s
 ✔ Container katalog-uji-db-1 Created                                 0.1s
 ✔ Container katalog-uji-db-1 Started                                 0.5s
 ✔ Container katalog-uji-web-1 Created                                 0.0s
 ✔ Container katalog-uji-web-1 Started                                 0.4s

Penulisan -p katalog-uji pada perintah terakhir sebenarnya mubazir — nama folder katalog-uji sudah otomatis menjadi nama project — tetapi menuliskannya eksplisit membuat identitas project tidak lagi bergantung pada kebetulan nama folder tempat berkas berada. Kedua project kini berdiri berdampingan, terbaca dalam satu inventaris:

bash
$ docker compose ls

Output yang Dihasilkan:

text
NAME          STATUS        FILES
katalog       running(2)    /home/budi/katalog/compose.yaml
katalog-uji   running(2)    /home/budi/katalog-uji/compose.yaml

Lingkungan uji selesai dipakai dan dibongkar dari dalam foldernya sendiri, lalu Budi kembali ke folder utama:

bash
$ docker compose down
$ cd ../katalog

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Container katalog-uji-web-1 Removed                                0.3s
 ✔ Container katalog-uji-db-1 Removed                                 0.5s
 ✔ Network katalog-uji_default Removed                                0.2s

container_name: Nama Tetap #

Pola <project>-<service>-<index> bisa diganti dengan nama pilihan kita lewat kunci container_name:

yaml
services:
  db:
    image: postgres:18-alpine
    container_name: katalog-db

Dengan deklarasi itu, container akan bernama katalog-db — tanpa prefix project dan tanpa index. Nama yang tetap memang nyaman bagi skrip luar yang memanggil docker logs katalog-db atau docker exec katalog-db psql ..., tetapi kenyamanannya dibayar mahal:

Menskalakan Replika dengan –scale #

Ujung dari cerita index adalah kemampuan yang dibuatnya mungkin: satu service, banyak container. Perintah berikut meminta tiga replika web:

bash
$ docker compose up -d --scale web=3

Output yang Dihasilkan:

text
[+] Running 4/4
 ✔ Container katalog-db-1 Running                                    0.0s
 ✔ Container katalog-web-1 Running                                   0.0s
 ✔ Container katalog-web-2 Created                                   0.1s
 ✔ Container katalog-web-3 Created                                   0.1s
Error response from daemon: driver failed programming external connectivity on endpoint katalog-web-2 (4b7e2d9c8a1f): Bind for 0.0.0.0:8080 failed: port is already allocated

Aritmetikanya memang tidak bisa menipu. Pemetaan "8080:80" menyediakan tepat satu pintu host, sedangkan --scale web=3 menghadirkan tiga container yang masing-masing menuntut pintu sendiri; katalog-web-1 sudah memegang 8080, sehingga katalog-web-2 gugur saat Compose mencoba mengikatkan port yang sama — dan katalog-web-2 serta katalog-web-3 tertinggal berstatus Created tanpa pernah menyala. Solusinya adalah menyediakan rentang pintu yang sama banyaknya dengan replika:

bash
$ docker compose down
$ sed -i 's/"8080:80"/"8080-8082:80"/' compose.yaml
$ docker compose up -d --scale web=3

Output yang Dihasilkan:

text
[+] Running 5/5
 ✔ Network katalog_default Created                                    0.2s
 ✔ Container katalog-db-1 Started                                     0.5s
 ✔ Container katalog-web-1 Started                                    0.4s
 ✔ Container katalog-web-2 Started                                    0.3s
 ✔ Container katalog-web-3 Started                                    0.3s

Baris-baris Created dipotong agar ringkas — keduanya hadir di antara network dan baris Started seperti pada output-output sebelumnya. Rentang 8080-8082 dibagi rata: replika pertama menerima 8080, kedua 8081, ketiga 8082. Inventarisnya terbaca dalam satu tabel:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME             IMAGE               COMMAND                  SERVICE   CREATED              STATUS              PORTS
katalog-db-1     postgres:18-alpine  "docker-entrypoint.s…"   db        About a minute ago   Up About a minute   5432/tcp
katalog-web-1    nginx:1.29-alpine   "/docker-entrypoint.…"   web       About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
katalog-web-2    nginx:1.29-alpine   "/docker-entrypoint.…"   web       About a minute ago   Up About a minute   0.0.0.0:8081->80/tcp, [::]:8081->80/tcp
katalog-web-3    nginx:1.29-alpine   "/docker-entrypoint.…"   web       About a minute ago   Up About a minute   0.0.0.0:8082->80/tcp, [::]:8082->80/tcp

Ketiga pintu benar-benar hidup dan melayani halaman yang sama:

bash
$ curl -s -o /dev/null -w "%{http_code}\n" localhost:8080 localhost:8081 localhost:8082

Output yang Dihasilkan:

text
200
200
200

Kembali ke satu replika adalah satu perintah — replika kelebihan dibongkar otomatis:

bash
$ docker compose up -d --scale web=1

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Container katalog-web-3 Removed                                   0.2s
 ✔ Container katalog-web-2 Removed                                   0.2s
 ✔ Container katalog-web-1 Running                                   0.0s

Budi mengembalikan pemetaan port berkas ke bentuk semula dengan sed -i 's/"8080-8082:80"/"8080:80"/' compose.yaml, karena katalog hanya membutuhkan satu pintu web. Satu batas penting perlu dicatat sebelum chapter ditutup: skala cocok untuk service yang stateless seperti web, tetapi --scale db=3 bukan cara membuat cluster database. Tiga replika postgres akan berjalan sebagai tiga server yang saling asing dengan writable layer masing-masing — data tercecer tiga tempat tanpa mekanisme replikasi; database adalah service stateful, dan pola pengelolaannya yang benar menanti di Section Docker dan Database.

Cetakbiru web pada berkas katalog kini memakai satu image siap pakai nginx:1.29-alpine — pengganti sementara yang kita janjikan sejak chapter pembuka. Service yang sama bisa saja membangun imagenya sendiri dari Dockerfile di dalam proyek, menarik versi terkunci dengan digest, atau mengatur kapan image harus ditarik ulang. Chapter Image, Build, dan Pull mengganti pengganti sementara itu dengan aplikasi katalog yang sesungguhnya.

Referensi #