Stack katalog kini sehat secara terukur: data bertahan di volume, urutan start terjaga gerbang service_healthy, dan dua probe menjunjung kejujuran status. Dua kebutuhan terakhir masih memakai jalan yang kurang rapi. Berkas konfigurasi utuh — kelak dibutuhkan begitu Budi menaruh reverse proxy nginx di depan web sebagai satu-satunya pintu masuk — selama ini hanya bisa dibind manual seperti pola Chapter Bind Mount, bukan dideklarasikan sebagai bagian project. Sementara itu password database masih berpindah sebagai string polos di environment: terbaca printenv, terbaca docker inspect, dan tercatat di .env — Siti sampai mengingatkan bahwa siapa pun yang membuka mesin development-nya bisa membaca rahasia123 dalam satu perintah. Compose menyediakan dua mekanisme khusus untuk keduanya: configs untuk menyuntikkan berkas konfigurasi, dan secrets untuk membagikan data sensitif sebagai berkas — keduanya dibedah tuntas di chapter ini.

configs: Berkas Konfigurasi sebagai Objek Project #

Mekanisme configs memperlakukan satu berkas di host sebagai objek milik project — dideklarasikan di level teratas, lalu dipasang ke service yang membutuhkannya sebagai berkas read-only di dalam container. Kita mulai dari konfigurasinya sendiri: satu nginx.conf minimal yang meneruskan seluruh permintaan ke service web:

conf
server {
    listen 80;

    location / {
        proxy_pass http://web:3000;
    }
}

Mari kita telaah tiga baris inti berkas di atas:

  1. server { listen 80; ... } — satu blok server yang menerima koneksi di port 80 milik container proxy; port inilah yang akan kita terbitkan ke host.
  2. location / { ... } — blok yang cocok untuk seluruh jalur permintaan, dari / sampai /healthz; tidak ada pengecualian.
  3. proxy_pass http://web:3000; — jantung konfigurasi ini: nginx meneruskan permintaan ke alamat web:3000; nama service web di-resolve oleh DNS network katalog_default, sehingga proxy tidak pernah perlu tahu alamat IP container yang bisa berganti-ganti.

Service proxy dan deklarasi teratasnya berbentuk sebagai berikut:

yaml
  proxy:
    image: nginx:1.29-alpine
    ports:
      - "8090:80"
    configs:
      - source: nginx-conf
        target: /etc/nginx/conf.d/default.conf
    depends_on:
      - web

configs:
  nginx-conf:
    file: ./nginx.conf

Mari kita telaah bagian-bagian yang bekerja sama di atas:

  1. configs level teratas — mendeklarasikan objek bernama nginx-conf yang isinya diambil dari ./nginx.conf; path dihitung relatif terhadap lokasi compose.yaml, sama seperti aturan path pada bind mount.
  2. source: nginx-conf di service — merujuk objek teratas tersebut; service boleh memasang config yang sama berkali-kali dengan target berbeda, dan service lain boleh ikut memakainya tanpa mendeklarasikan ulang isinya.
  3. target: /etc/nginx/conf.d/default.conf — path LENGKAP berkas di dalam container, bukan direktorinya; image nginx memuat seluruh *.conf dari direktori /etc/nginx/conf.d ke dalam blok http, dan berkas default.conf bawaan image berisi server halaman-statis yang kita ganti sepenuhnya dengan proxy ini.
  4. Bila target tidak ditulis — berkas dipasang di root container dengan nama objeknya (/nginx-conf), path yang tidak dibaca nginx sama sekali; karena itu target lengkap bukan gaya penulisan, melainkan syarat kerja.
  5. ports: "8090:80" — port host 8090 dipilih agar tidak bertabrakan dengan 8080 yang sudah diterbitkan web; dua service tidak boleh merebut port host yang sama.
  6. depends_on bentuk daftar — proxy menyala setelah container web berjalan; gerbang service_healthy tidak diperlukan di sini karena nginx tetap hidup meski upstream-nya sesaat belum siap — ia menjawab 502 Bad Gateway sebentar lalu mulus begitu web angkattelepon.

Waktunya menyaksikan proxy bekerja:

bash
$ docker compose up -d proxy

Output yang Dihasilkan:

text
[+] Running 4/4
 ✔ Network katalog_default    Running
 ✔ Container katalog-db-1     Running
 ✔ Container katalog-web-1    Running
 ✔ Container katalog-proxy-1  Started
bash
$ curl localhost:8090

Output yang Dihasilkan:

text
<h1>Katalog Produk</h1>
<ul>
  <li>Kopi Arabika Gayo — Rp85.000</li>
...

Isi yang sama dengan curl localhost:8080 kini tiba lewat jalur baru: permintaan masuk ke host 8090, diterima nginx di port 80 container proxy, diteruskan lewat proxy_pass ke web:3000, lalu jawabannya menempuh jalan pulang yang sama. Validasi konfigurasi dari dalam container memastikan berkas yang terbaca nginx memang berkas kita:

bash
$ docker compose exec proxy nginx -t

Output yang Dihasilkan:

text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: the configuration file /etc/nginx/nginx.conf test is successful
bash
$ docker compose exec proxy cat /etc/nginx/conf.d/default.conf

Output yang Dihasilkan:

text
server {
    listen 80;

    location / {
        proxy_pass http://web:3000;
    }
}

Berkas yang kita simpan di host tampil utuh di dalam container — tanpa COPY saat build, tanpa bind mount yang path-nya harus dijaga. Config dipasang read-only persis seperti bind mount read_only: true, sehingga container tidak dapat mengubah konfigurasi yang diberikan kepadanya. Peran reverse proxy yang lebih dewasa — TLS, beberapa upstream, kompresi — menjadi petualangan tersendiri di Chapter Reverse Proxy pada Section Multi-Container Application.

secrets: Rahasia sebagai Berkas #

Config menjawab kebutuhan berkas; kini giliran data yang tidak boleh terpampang. Password di environment variable memang praktis, tetapi nilai polosnya terbaca printenv, docker inspect, dan docker compose config — kelemahan yang sudah kita tandai sejak Chapter Environment Variable. Mekanisme secrets mengubah bentuk pembawaannya: nilai hidup di dalam sebuah berkas, berkas itu dipasang read-only di path bawaan /run/secrets/<nama>, dan environment variable yang tersisa hanyalah alamat berkasnya — bukan isinya.

Budi memindahkan password database ke berkas db_password.txt:

bash
$ echo "rahasia123" > db_password.txt

Deklarasi teratas dan perubahan pada service db:

yaml
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: katalog
      POSTGRES_DB: katalog
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db-data:/var/lib/postgresql/data

secrets:
  db_password:
    file: ./db_password.txt

Mari kita telaah bagian-bagian di atas:

  1. secrets level teratas — objek bernama db_password yang isinya dibaca dari ./db_password.txt pada saat container diciptakan; strukturnya seiman dengan configs, dan perbedaan keduanya kita rangkum di tabel di akhir chapter.
  2. secrets: - db_password di service — bentuk pendek tanpa target; secret dipasang di path bawaan /run/secrets/db_password, konvensi path yang sama di mana pun container berjalan.
  3. POSTGRES_PASSWORD_FILE: /run/secrets/db_password — pola *_FILE yang didukung entry point image resmi postgres untuk kredensialnya; entry point membaca isi berkas itu sebagai pengganti POSTGRES_PASSWORD, sehingga string polos tidak pernah lagi menempel pada environment container.
  4. Volume db-data yang sudah terisi — password hanya dibaca saat initdb pertama pada volume kosong, perilaku yang sudah kita catat pada Chapter Volume; perubahan ini bermakna penuh pada pemasangan baru di mesin lain, meskipun cluster yang ada tidak tersentuh.
bash
$ docker compose up -d

Output yang Dihasilkan (dipotong):

text
[+] Running 4/4
 ✔ Network katalog_default    Running
 ✔ Container katalog-db-1     Recreated
 ✔ Container katalog-db-1     Healthy
 ✔ Container katalog-web-1    Recreated
 ✔ Container katalog-web-1    Started
 ...

Bukti bahwa secret benar-benar tiba di dalam container:

bash
$ docker compose exec db cat /run/secrets/db_password

Output yang Dihasilkan:

text
rahasia123

Sedangkan upaya menulis balik ditolak keras:

bash
$ docker compose exec db sh -c 'echo uji >> /run/secrets/db_password'

Output yang Dihasilkan:

text
sh: can't create /run/secrets/db_password: Read-only file system

configs versus secrets #

Kedua mekanisme itu terlihat kembar — sama-sama berkas dari host, sama-sama dipasang read-only — tetapi tujuannya berbeda arah. Tabel berikut memisahkan keduanya:

Aspekconfigssecrets
Isi yang layakKonfigurasi non-sensitif — nginx.conf, templateData sensitif — password, token, kunci TLS
Path bawaan di container/<nama> bila target tidak ditulis/run/secrets/<nama>
IzinDipasang read-onlyRead-only; mode bawaan 0444 di Swarm, dapat dipersempit lewat mode
Nasib di produksiAman dibagikan sebagai berkas biasaDi luar Swarm tetap berkas polos di host — produksi sungguhan memakai secret manager

Pembeda terpenting bukan mekanisme, melainkan makna: config boleh dibaca siapa pun tanpa kerugian, secret dirancang untuk dibaca hanya oleh proses yang berhak. Di Swarm, perbedaan itu diterjemahkan menjadi perlakuan distribusi yang berbeda — secret dienkripsi dalam perjalanan dan hanya dikirim ke node yang menjalankan service yang memintanya, sedangkan config didistribusikan sebagai berkas biasa.

Dengan configs dan secrets, dua jalan pintas terakhir pun tertutup: konfigurasi utuh kini berstatus objek project, dan kredensial berpindah bentuk dari string yang terpampang menjadi berkas yang terpasang read-only. Berkas katalog kita berakhir dengan empat deklarasi teratas — services, volumes, configs, secrets — masing-masing menjaga kelas barangnya sendiri. Namun masih ada satu pola yang belum terwakili: bagaimana bila kita ingin service yang hanya menyala saat dibutuhkan — misalnya utilitas browsing database untuk mengintip isi tabel produk ketika insiden terjadi, yang tidak boleh ikut menyala pada setiap docker compose up produksi? Compose menyimpan jawabannya di kunci profiles, pembuka Chapter Profiles.

Referensi #