Berkas katalog kini memuat hampir semua yang kita butuhkan — tetapi juga menumpuk dua kepentingan yang mulai saling mengganjal. Di satu sisi ada konfigurasi bersama yang dibaca seluruh tim: service, port, volume, healthcheck, gerbang dependensi. Di sisi lain ada kebiasaan personal Budi yang tertanam di berkas yang sama: command: node --watch src/server.js dan blok develop.watch yang hanya bermakna di mesin pengembang. Siti tidak pernah menjalankan watch di laptopnya, dan server produksi sama sekali tidak boleh menjalankan aplikasi dengan mode pemantau berkas. Menyunting satu berkas untuk setiap kebutuhan berarti berkas itu berhenti menjadi sumber kebenaran bersama; memelihara banyak salinan berarti perubahan kecil harus disalin ke mana-mana. Compose menutup section ini dengan mekanisme untuk keluar dari dilema itu: berkas override yang dimuat otomatis untuk personalisasi, aturan penggabungan yang bisa diprediksi, dan tag kendali untuk kasus yang menuntut pengecualian tegas.
compose.override.yaml Dimuat Otomatis #
Aturan dasarnya sederhana: ketika Compose dijalankan tanpa flag -f, ia memuat compose.yaml lalu mencari satu nama tambahan — compose.override.yaml — di folder yang sama, dan bila ditemukan, kedua berkas digabung menjadi satu konfigurasi. Pemilihan berkasnya mengikuti alur berikut:
Mari kita telaah diagram di atas: cabang kiri adalah mode harian — nama override yang istimewa dimuat tanpa diminta; cabang kanan adalah mode eksplisit — daftar berkas yang kita tulis sendiri menjadi satu-satunya yang dibaca, dan nama istimewa itu diabaikan. Budi memanfaatkan cabang kiri dengan memindahkan seluruh kebiasaan pengembangannya keluar dari compose.yaml. Service web pada berkas utama kembali bersih — build, ports, env_file, environment, restart, depends_on, healthcheck — sementara dua kunci pribadinya pindah ke compose.override.yaml yang baru dibuatnya:
services:
web:
command: node --watch src/server.js
develop:
watch:
- path: ./src
action: sync
target: /app/src
ignore:
- node_modules/
- path: ./package.json
action: rebuild
- path: ./Dockerfile
action: rebuildKedua berkas itu kini menjalankan pembagian tugas yang tegas: compose.yaml milik semua orang, compose.override.yaml milik Budi. Hasil penggabungannya bisa diperiksa lewat perintah render yang sudah menjadi kebiasaan:
$ docker compose configOutput yang Dihasilkan (dipotong):
name: katalog
services:
db:
...
web:
build:
context: /home/budi/katalog
command:
- node
- --watch
- src/server.js
develop:
watch:
- action: sync
ignore:
- node_modules/
path: /home/budi/katalog/src
target: /app/src
- action: rebuild
path: /home/budi/katalog/package.json
- action: rebuild
path: /home/budi/katalog/Dockerfile
environment:
DATABASE_URL: postgres://katalog:rahasia123@db:5432/katalog
...Mari kita telaah tiga normalisasi pada hasil render tersebut:
commandyang ditulis sebagai satu string menjadi daftar — renderer selalu mencetak perintah dalam exec form, memecahnode --watch src/server.jsmenjadi tiga elemen; bentuk tulis bebas, bentuk hasil selalu daftar, persis perilakuenvironmentyang kita saksikan pada Chapter Struktur compose.yaml.- Path relatif dari kedua berkas diselesaikan lalu dicetak absolut —
./srcpada override dan.padabuildsama-sama diterjemahkan ke folder berkas utama,/home/budi/katalog; hasil gabungan tidak pernah menyimpan path relatif yang ambigu. environmenttampil dengan nilai hasil interpolasi —rahasia123tercetak polos, pengingat terakhir dari peringatan Chapter Variable Interpolation bahwa keluaran render bukan tempat untuk dipajang.
Uji ketegasan pembagian ini pun murah. Budi menyalakan stack dan menjalankan penyelarasan kedua:
$ docker compose up -dOutput yang Dihasilkan:
[+] Running 4/4
✔ Network katalog_default Running
✔ Container katalog-db-1 Running
✔ Container katalog-web-1 Running
✔ Container katalog-proxy-1 RunningTidak ada satu pun baris Recreated — hasil penggabungan kedua berkas identik dengan konfigurasi lama, sehingga daemon tidak menemukan selisih apa pun untuk diwujudkan. Perpindahan command dan develop ke berkas baru tidak mengubah hasil akhir sedikit pun, dan itulah inti mekanismenya: override mengubah dari mana konfigurasi dibaca, bukan apa isinya.
Aturan Penggabungan Dua Berkas #
Penggabungan bukan penimpaan berkas demi berkas, melainkan peleburan per kunci konfigurasi. Aturannya mengikuti bentuk nilai tiap kunci:
| Bentuk nilai pada kunci | Perilaku penggabungan bawaan | Contoh kunci |
|---|---|---|
| Peta (mapping) | digabung per kunci — deep merge; kunci yang ditulis ulang ditimpa nilai berkas belakangan | environment, labels |
| Sequence umum | diganti total oleh berkas belakangan | command, env_file, volumes service |
| Sequence multi-nilai | digabungkan — union tanpa duplikat | ports, expose, dns, tmpfs |
| Skalar | nilai berkas belakangan menang | image, restart, working_dir |
| Resource bernama level teratas | digabung per nama | volumes, networks, configs, secrets |
Tiga konsekuensi praktis dari tabel itu layak dihafal karena menentukan cara menulis override yang benar:
- Tulis
environmentdalam bentuk peta — peta digabung per kunci, sehingga override cukup menyebut variabel yang berubah; bentuk daftar akan mengganti total, dan seluruh variabel lain lenyap. Inilah pemenuhan janji TIP pada Chapter Environment dan env File yang menyuruh kita membiasakan bentuk peta sejak awal. volumespada override harus daftar lengkap — sequence umum diganti total; override yang lupa mengulangdb-data:/var/lib/postgresql/dataakan membuat servicedbberjalan tanpa volume datanya, dan kehilangan itu tidak mengeluarkan satu galat pun.portsdigabung, bukan diganti — menambah port baru pada override tidak menyingkirkan port dari berkas utama; keduanya diterbitkan bersamaan. Konsekuensi ini yang paling sering mengejutkan, dan menjadi alasan utama tag kendali pada bagian berikutnya ada.
Tag !reset dan !override #
Dua aturan penggabungan itu kadang bukan yang kita inginkan. Untuk dua titik friksi paling sering, Compose menyediakan tag khusus yang dituliskan langsung pada nilai: !reset menghapus sebuah kunci dari hasil penggabungan, dan !override memaksa sebuah sequence diganti total alih-alih digabung. Bentuk pemakaiannya:
services:
web:
build: !reset null
ports: !override
- "${BIND_IP:-0.0.0.0}:80:3000"Mari kita telaah kedua tag di atas:
build: !reset null— menghapus kuncibuilddari hasil merge seolah kunci itu tidak pernah ditulis di berkas mana pun; service akan sepenuhnya mengandalkanimagedari registry. Tanpa tag ini,builddanimageberdiri berdampingan — perilaku yang sah di development, tapi bencana di server yang tidak punya source code.ports: !overridediikuti daftar nilai — mematikan aturan union untuk kunci ini; daftar pada berkas belakangan menjadi satu-satunya yang berlaku dan port dari berkas utama tidak pernah diterbitkan.- Tag dibaca saat merge dan hilang dari hasil render — keluaran
docker compose confighanya memuat nilai akhirnya; tag adalah instruksi bagi Compose, bukan bagian konfigurasi final.
Varian Produksi compose.prod.yaml #
Sekarang cerita lengkapnya. Budi ingin menguji varian produksi katalog pada server kecil — image ditarik dari registry, aplikasi melayani port standar 80, dan kebiasaan development ditinggalkan sepenuhnya. Draf pertamanya sederhana:
services:
web:
image: budi/katalog-web:${IMAGE_TAG:-latest}
ports:
- "${BIND_IP:-0.0.0.0}:80:3000"Ia menjalankannya sebagai project terpisah — flag -p katalog-prod memberi identitas project yang berbeda dari stack development yang sedang menyala, persis pola -p katalog-uji yang kita praktikkan pada Chapter Service:
$ docker compose -f compose.yaml -f compose.prod.yaml -p katalog-prod up -d webOutput yang Dihasilkan (dipotong):
[+] Building 9.1s (14/14) FINISHED
...
=> => naming to docker.io/budi/katalog-web:latest
✔ Container katalog-prod-db-1 Healthy
Error response from daemon: driver failed programming external connectivity on endpoint katalog-prod-web-1 (e6d94b2c8a15): Bind for 0.0.0.0:8080 failed: port is already allocatedGalat itu adalah konflik port lama kita, dan biang keroknya adalah aturan penggabungan ports yang baru saja kita pelajari. Mari kita telaah dua kesalahan draf pertama:
portsdigabung, bukan diganti — hasil merge memuat dua pemetaan:8080:3000dari compose.yaml dan80:3000dari draf; port host 8080 sedang dipegangkatalog-web-1milik project katalog, dan satu pintu host tidak boleh dipakai dua pemetaan — galatBind for 0.0.0.0:8080 failed: port is already allocatedyang kita kenal sejak Section Jaringan Container.builddari berkas utama masih aktif — output memuat baris build karena kunci itu tidak dihapus; di laptop Budi kebetulan tidak masalah karena Dockerfile hadir, tetapi di server tanpa source code, perintah yang sama akan gugur jauh sebelum menyentuh port.
Sisa kegagalan itu dibersihkan lebih dulu — docker compose -p katalog-prod down membongkar container db yang sempat menyala beserta networknya — lalu draf kedua menuntaskan keduanya:
services:
web:
build: !reset null
image: budi/katalog-web:${IMAGE_TAG:-latest}
ports: !override
- "${BIND_IP:-0.0.0.0}:80:3000"
restart: unless-stopped
db:
restart: unless-stoppedHasil penggabungannya diverifikasi sebelum dijalankan:
$ docker compose -f compose.yaml -f compose.prod.yaml configOutput yang Dihasilkan (dipotong):
services:
web:
environment:
DATABASE_URL: postgres://katalog:rahasia123@db:5432/katalog
image: budi/katalog-web:latest
ports:
- mode: ingress
target: 3000
published: "80"
protocol: tcp
host_ip: 0.0.0.0
restart: unless-stopped
...Mari kita telaah empat bukti pada hasil render tersebut:
- Kunci
buildtidak hadir sama sekali —!reset nullmenghapusnya dari hasil merge; servicewebkini murni bergantung padaimage: budi/katalog-web:latest, dan di server sungguhan image itu ditarik dari registry tanpa satu berkas source pun. portshanya memuat satu pemetaan —published: "80"denganhost_iphasil interpolasiBIND_IP; port 8080 milik development tidak pernah diterbitkan — bukti!overridemengalahkan aturan union.restart: unless-stopped— kebijakan bangkit otomatis untuk server yang boleh di-reboot, melunasi pola dari Chapter Restart Policy pada konteks produksi.commanddandeveloptidak hadir — keduanya kini hanya hidup di compose.override.yaml, dan berkas itu tidak ikut dimuat.
Poin keempat itu cukup penting untuk ditegaskan sendiri:
Eksekusi kedua pun berjalan mulus:
$ docker compose -f compose.yaml -f compose.prod.yaml -p katalog-prod up -d webOutput yang Dihasilkan:
[+] Running 3/3
✔ Network katalog-prod_default Created
✔ Container katalog-prod-db-1 Healthy
✔ Container katalog-prod-web-1 StartedPerintah ini hanya menyasar web — proxy tidak disebut dan tidak dinyalakan, pola target eksplisit yang kita kenal sejak Chapter Config dan Secret; db tetap ikut menyala karena gerbang depends_on menuntutnya sehat lebih dulu. Hasilnya bisa diuji dari luar:
$ curl localhost:80Output yang Dihasilkan:
<h1>Katalog Produk</h1>
<ul>
</ul>Daftar produknya kosong — dan itu justru bukti isolasi yang bekerja: volume yang dipasang db project ini adalah katalog-prod_db-data, objek baru hasil initdb, sementara data development tetap tinggal aman di katalog_db-data milik project katalog. Prefix nama project memisahkan bukan hanya container dan network, melainkan seluruh kelas objek — persis aturan penamaan dari Chapter Volume dan Bind Mount.
Dua Project Berdampingan dengan -p #
Kedua project kini berbagi satu daemon, dan inventarisnya terbaca dalam satu tabel:
$ docker compose lsOutput yang Dihasilkan:
NAME STATUS FILES
katalog running(3) /home/budi/katalog/compose.yaml
katalog-prod running(2) /home/budi/katalog/compose.yaml,/home/budi/katalog/compose.prod.yamlMari kita telaah keadaan berdampingan tersebut:
- Kolom FILES memperlihatkan asal konfigurasi tiap project — project katalog membaca satu berkas, sedangkan katalog-prod membaca gabungan dua berkas yang ditunjuk flag
-f; satu folder yang sama memelihara dua wajah stack. - Dua project berbagi satu ruang nomor port host — katalog memegang 8080 dan 8090, katalog-prod memegang 80; bila keduanya menuntut port yang sama, galat
port is already allocatedmuncul sebagaimana tadi kita alami — jalan keluarnya adalah nilai port yang berbeda per varian, dan!overrideplus${BIND_IP}adalah alat yang tepat untuk itu. -pbisa digantikanCOMPOSE_PROJECT_NAME— variabel environment dari Chapter Variable Interpolation ini menyetel nama project tanpa flag, dan di server sungguhan Budi cukup menaruhnya di.envagar setiap perintah membawa identitas yang sama.
Sesi uji selesai; varian produksi dibongkar dengan perintah yang membawa identitas projectnya sendiri — docker compose -f compose.yaml -f compose.prod.yaml -p katalog-prod down — dan stack development di project katalog tetap menyala tanpa tersentuh.
Struktur Folder Final dan Kebijakan Commit #
Seluruh perjalanan section ini bermuara pada satu struktur folder yang final:
katalog/
├── compose.yaml
├── compose.override.yaml
├── compose.prod.yaml
├── .env
├── .env.example
├── .gitignore
├── Dockerfile
├── package.json
├── nginx.conf
├── db_password.txt
└── src/
└── server.jsNasib tiap berkas di repositori mengikuti pembagian perannya:
| Berkas | Nasib di Git | Alasan |
|---|---|---|
compose.yaml, compose.prod.yaml | di-commit | konfigurasi bersama seluruh tim dan seluruh lingkungan |
Dockerfile, package.json, src/, nginx.conf | di-commit | aplikasi beserta resep buildnya |
.env.example, .gitignore | di-commit | template nilai dan daftar pengecualian |
compose.override.yaml | TIDAK di-commit | personalisasi per orang — tambahkan ke .gitignore |
.env, db_password.txt | TIDAK di-commit | nilai per mesin dan rahasia — janji dari Chapter Environment dan env File dan Config dan Secret |
Kebijakan “override tidak di-commit” punya pengecualian yang disengaja: bila konfigurasi development ingin dibagikan ke seluruh tim — watcher, port dev, service debug — simpan dengan nama lain seperti compose.dev.yaml dan muat eksplisit dengan docker compose -f compose.yaml -f compose.dev.yaml up -d, atau set COMPOSE_FILE=compose.yaml:compose.dev.yaml agar kombinasi itu menjadi bawaan shell. Nama yang tidak istimewa tidak pernah dimuat diam-diam, dan itu tepatnya yang kita inginkan: berkas yang mempengaruhi semua orang harus disebut dengan sengaja.
Section Docker Compose sampai di sini menuntaskan seluruh perjalanannya. Kita bermula dari deretan docker run dua belas baris yang panjang dan rapuh, lalu mengubahnya menjadi satu berkas deklaratif: service sebagai cetakbiru, image siap pakai dan hasil build sendiri, port dan network dengan DNS antar service, volume yang menjaga data tetap hidup, environment yang tersuntik dari .env, urutan start yang dijaga gerbang kesehatan, config dan secret sebagai berkas milik project, profiles untuk service yang hanya menyala saat dipanggil, interpolation agar satu berkas melayani banyak mesin, watch yang menekan siklus edit-uji menjadi hitungan detik, dan override yang memisahkan kebiasaan personal dari konfigurasi bersama. Stack katalog — tiga service, satu berkas utama, dua berkas pendamping — adalah bukti bahwa seluruh perangkat itu bekerja sebagai satu kesatuan. Namun katalog masih aplikasi latihan dua service sesungguhnya; aplikasi dunia nyata berdiri dengan arsitektur yang lebih kaya: reverse proxy sebagai satu-satunya pintu, beberapa aplikasi di belakangnya dengan database masing-masing, cache untuk menjawab pertanyaan yang berulang, dan antrian untuk pekerjaan yang tidak harus selesai saat itu juga. Merakit dan mengoperasikan arsitektur seperti itulah kerja Section Aplikasi Multi-Container — dimulai dari Chapter Arsitektur Multi-Container, lalu Reverse Proxy, Application dan Database, sampai Cache dan Queue, dengan seluruh perangkat Compose yang kini sudah lengkap di tangan kita.
Referensi #
- Multiple Compose files — pemuaatan otomatis override, urutan berkas, dan aturan penggabungan.
- The Compose Specification
— definisi formal merge beserta tag khusus
!resetdan!override. - docker compose config — verifikasi hasil penggabungan sebelum dijalankan.
- docker compose up
— flag
-f,-p, dan perilaku penyelarasan.