Data katalog kini berumur panjang di dalam volume katalog_db-data, dan docker compose down tidak lagi membawa kabar buruk. Namun stack kita masih menyimpan satu kekosongan yang justru makin terasa: service web belum tahu apa-apa tentang tetangga db-nya — alamat host, port, nama database, sampai kredensial user semuanya belum pernah diberitahukan. Kebutuhan semacam ini tidak diselesaikan dengan mount, melainkan dengan environment variable yang disuntikkan Compose ke container pada saat diciptakan — mekanisme yang kita kenal dari flag -e pada Chapter Environment Variable, kini dalam bentuk deklaratif. Chapter ini membedah tiga pintunya sekaligus: kunci environment dengan dua bentuk tulisnya, env_file untuk daftar panjang dalam berkas terpisah, dan berkas .env yang — rawan membingungkan — ternyata bisa memainkan dua peran yang sama sekali berbeda.

Dua Bentuk Kunci environment #

Kunci environment di dalam service menerima dua tata tulis. Bentuk daftar menuliskan setiap variabel sebagai string KUNCI=nilai, sedangkan bentuk peta menuliskannya sebagai pasangan kunci-nilai YAML:

yaml
  db:
    environment:
      - POSTGRES_USER=katalog
      - POSTGRES_PASSWORD=rahasia123
      - POSTGRES_DB=katalog
yaml
  db:
    environment:
      POSTGRES_USER: katalog
      POSTGRES_PASSWORD: rahasia123
      POSTGRES_DB: katalog
AspekBentuk daftarBentuk peta
Tata tulis- KUNCI=nilai, satu baris satu variabelKUNCI: nilai, pasangan YAML
Nilai yang tampak seperti angkaSelamat — seluruh baris adalah stringPerlu dikutip: PGPORT: "5432"
Urutan variabelTerjaga persis seperti ditulisTidak berarti — peta tidak berurutan
Perilaku saat merge berkas overrideDaftar lama diganti totalDigabung per kunci, nilai terbaru menang

Menyambungkan web ke db lewat DATABASE_URL #

Banyak aplikasi Node.js membaca alamat koneksi databasenya dari satu variabel DATABASE_URL. Aplikasi katalog kita termasuk yang demikian — kodenya membentuk pool koneksi dari variabel itu:

js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

Karena itu, menyambungkan web ke db cukup satu baris pada service web:

yaml
  web:
    build: .
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://katalog:rahasia123@db:5432/katalog

Mari kita telaah isi URL di atas segmen demi segmen:

  1. postgres:// — skema koneksi; library pg memakainya untuk memilih protokol dan bentuk parameter.
  2. katalog:rahasia123 — user dan password, dipisah titik dua, sama dengan nilai POSTGRES_USER dan POSTGRES_PASSWORD yang membangun cluster sejak awal.
  3. db — nama host; di dalam network katalog_default, nama service db di-resolve menjadi alamat container database — mekanisme DNS yang kita bedah pada Chapter Port dan Network.
  4. 5432 — port internal PostgreSQL; komunikasi antar service berjalan di port container, bukan port host 8080 yang hanya milik web.
  5. /katalog — nama database yang dibuka setelah koneksi sah.

Terapkan perubahan, lalu validasi nilainya langsung dari dalam container:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog_default  Running
 ✔ Container katalog-db-1   Running
 ✔ Container katalog-web-1  Recreated
 ✔ Container katalog-web-1  Started
bash
$ docker compose exec web printenv DATABASE_URL

Output yang Dihasilkan:

text
postgres://katalog:rahasia123@db:5432/katalog

Perhatikan bahwa hanya web yang dibuat ulang — service db tidak tersentuh karena konfigurasinya tidak berubah. Bukti akhirnya datang dari aplikasi itu sendiri, yang kini berhasil membaca tabel produk:

bash
$ curl localhost:8080

Output yang Dihasilkan:

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

env_file: Daftar Environment dalam Berkas Terpisah #

Bayangkan daftar environment web membesar: mode logging, batas halaman, kunci API pembayaran, alamat layanan email. Menempel semuanya di compose.yaml membuat berkas konfigurasi sesak oleh nilai-nilai yang sifatnya per-lingkungan. Compose menyediakan pintu kedua berupa env_file — berkas teks berisi baris KUNCI=nilai yang dimuat ke dalam environment container:

text
POSTGRES_PASSWORD=rahasia123
LOG_LEVEL=info

Budi menyimpan berkas itu sebagai .env di root project, lalu mendaftarkannya pada service web:

yaml
  web:
    build: .
    ports:
      - "8080:3000"
    env_file:
      - .env
    environment:
      DATABASE_URL: postgres://katalog:rahasia123@db:5432/katalog

Compose membuat ulang container web dengan pola output yang sudah kita kenal (Recreated lalu Started), dan variabel dari berkas itu pun hidup di dalam container:

bash
$ docker compose exec web printenv LOG_LEVEL

Output yang Dihasilkan:

text
info

Mari kita telaah perilaku env_file di atas:

  1. Setiap baris KUNCI=nilai menjadi environment variable container — formatnya sama persis dengan --env-file pada docker run yang kita pakai di Chapter *Environment Variable; baris berawalan #` diabaikan sebagai komentar.
  2. Seluruh isi berkas ikut disuntikkan — POSTGRES_PASSWORD ikut masuk ke web padahal service itu tidak membutuhkannya; bila tiap service hanya boleh menerima bagiannya, pisahkan berkasnya per service (web.env, worker.env) dan daftarkan masing-masing.
  3. Nilai environment tetap menang untuk kunci yang sama — DATABASE_URL tidak muncul di .env, tetapi bila muncul, nilai di blok environment yang akan dipakai container.

Dua Peran Berkas .env #

Di titik inilah kebingungan klasik menunggu siapa pun yang memakai Compose. Berkas bernama tepat .env di root project memperoleh perlakuan istimewa dari CLI Compose: dimuat otomatis sebagai sumber interpolasi — setiap ${VARIABEL} yang ditulis di compose.yaml digantikan nilainya dari berkas itu, sebelum konfigurasi dipakai untuk membuat container apa pun. Ini peran yang sama sekali berbeda dari env_file di atas: interpolasi mengerjakan teks compose.yaml, bukan environment container.

Budi memanfaatkan peran kedua itu untuk menghapus password yang tertulis polos di dua tempat:

yaml
services:
  web:
    build: .
    ports:
      - "8080:3000"
    env_file:
      - .env
    environment:
      DATABASE_URL: postgres://katalog:${POSTGRES_PASSWORD}@db:5432/katalog

  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: katalog
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: katalog
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
bash
$ docker compose up -d

Output yang Dihasilkan:

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

Tidak ada container yang dibuat ulang — nilai hasil interpolasi sama persis dengan nilai yang sebelumnya tertulis polos, dan Compose menilai perubahan dari konfigurasi yang sudah diselesaikan, bukan dari teks mentah berkasnya. Verifikasi dari sisi container memastikan nilai tetap utuh:

bash
$ docker compose exec db printenv POSTGRES_PASSWORD

Output yang Dihasilkan:

text
rahasia123

Tiga pintu environment yang kita miliki kini bisa dirangkum dalam satu tabel:

Aspekenvironmentenv_file.env untuk interpolasi
Nilai mengalir keEnvironment containerEnvironment containerTeks compose.yaml itu sendiri
Kapan dibacaSaat container diciptakanSaat container diciptakanSetiap kali perintah compose membaca berkas
Perlu didaftarkan di serviceYa — blok environmentYa — daftar env_fileTidak — nama .env di root project cukup
Bisa menimpaMenimpa env_file dan ENV imageMenimpa ENV imageNilai dari shell operator menimpa .env

Berkas .env milik katalog saat ini memang memainkan dua peran sekaligus: dibaca env_file untuk container web, dan dibaca interpolasi untuk ${POSTGRES_PASSWORD}. Keduanya sah, asalkan kita sadar sedang memakai dua mekanisme berbeda — satu mengisi environment container, satu mengisi teks konfigurasi.

Prioritas Sumber Environment #

Untuk satu kunci yang sama, dari mana nilai container diambil? Urutannya tegap:

UrutanSumberKeterangan
1 — menangenvironment pada compose.yamlTermasuk hasil merge berkas override yang dibelakangkan
2env_file yang didaftarkan serviceNilai berkas menimpa ENV bawaan image
3 — kalahENV pada Dockerfile imageNilai dasar sejak build

Ketentuan override berlapis juga berlaku pada penggabungan berkas konfigurasi: perintah docker compose -f compose.yaml -f compose.prod.yaml menggabungkan dua berkas, dan untuk blok environment bentuk peta, kunci yang ditulis ulang di berkas belakangan menang atas berkas depan — inilah lapisan prioritas paling atas yang kita sebut singkat tadi dan bedah penuh pada Chapter Compose Override dan Environment. Di sisi interpolasi, nilai yang sudah terpasang di shell operator menimpa nilai .env, karena Compose membaca shell lebih dulu — aturan lengkapnya menjadi materi Chapter Variable Interpolation.

Nilai Kosong versus Variabel Tak Diset #

Dua keadaan yang kelihatannya sama padahal berbeda nasib:

yaml
  web:
    environment:
      TOKEN_VERIFIKASI: ""

Mari kita telaah dua keadaan itu:

  1. TOKEN_VERIFIKASI: "" — container menerima variabel yang ada tetapi bernilai string kosong; printenv TOKEN_VERIFIKASI berhasil keluar dengan baris kosong, dan di Node.js process.env.TOKEN_VERIFIKASI === "" bernilai true.
  2. Variabel yang tidak ditulis sama sekali — variabelnya tidak ada di environment; process.env.LAINNYA === undefined, dan library validasi konfigurasi umumnya menolak dengan pesan seperti “required variable is missing”.
  3. Konsekuensinya pada validasi startup — aplikasi yang hanya memeriksa “variabel ada” akan meloloskan string kosong tanpa sengaja; pemeriksaan yang benar menguji nilai terisi, bukan sekadar keberadaan kuncinya.

Melihat Hasil Resolusi lewat docker compose config #

Perintah docker compose config — yang kita kenal sebagai alat validasi sintaks pada Chapter Struktur compose.yaml — juga menjadi cermin hasil akhir seluruh mekanisme environment ini:

bash
$ docker compose config

Output yang Dihasilkan (dipotong):

text
name: katalog
services:
  db:
    environment:
      POSTGRES_DB: katalog
      POSTGRES_PASSWORD: rahasia123
      POSTGRES_USER: katalog
    image: postgres:18-alpine
    ...
  web:
    build:
      context: .
    environment:
      DATABASE_URL: postgres://katalog:rahasia123@db:5432/katalog
    env_file:
      - .env
    ports:
      - mode: ingress
        target: 3000
        published: "8080"
    ...
volumes:
  db-data:
    name: katalog_db-data

Mari kita telaah apa yang diperlihatkan resolusi ini:

  1. Interpolasi sudah selesai — ${POSTGRES_PASSWORD} tampil sebagai rahasia123; tidak ada jejak ${...} yang tersisa pada keluarannya.
  2. environment tercetak terurun — Compose mengurutkan kunci secara alfabetis pada keluaran; urutan penulisan di berkas sumber memang tidak bermakna bagi hasil akhir.
  3. env_file tetap tercantum apa adanya — isi berkasnya tidak digabungkan ke environment pada keluaran; keduanya tetap dua sumber yang berbeda hingga container diciptakan.
  4. Volume tampil dengan nama penuhnya — db-data diresolusi menjadi katalog_db-data, pengingat bahwa prefix project berlaku pada seluruh objek.
  5. Keluaran ini memuat rahasia — rahasia123 terbaca polos; jangan pernah menempelkan keluaran docker compose config ke issue tracker atau pesan grup.
bash
$ cat .gitignore

Output yang Dihasilkan:

text
node_modules
.env
bash
$ cat .env.example

Output yang Dihasilkan:

text
POSTGRES_PASSWORD=ganti-dengan-password-anda
LOG_LEVEL=info

Setiap orang yang menyalin repositori cukup menyalin .env.example menjadi .env lalu mengisi nilainya sendiri. Sintaks interpolasi yang lebih jauh — nilai bawaan ${VAR:-default} sampai pesan galat ${VAR:?wajib diisi} — kita bedah tuntas pada Chapter Variable Interpolation.

Environment kini lengkap: web tahu alamat db, kredensial hidup di satu berkas, dan seluruh nilai bisa diaudit lewat docker compose config. Namun ada satu momen rawan yang belum kita sentuh: detik-detik pertama setelah docker compose up, ketika web menyala lebih cepat daripada db yang masih sibuk menjalankan initdb. Di mesin Budi kebetulan selalu mulus; di mesin baru yang volumenya masih segal, web justru mati dengan galat koneksi sebelum sempat melayani satu permintaan pun. Cara mendeklarasikan urutan start — dan batas dari padanya — adalah materi Chapter depends_on dan Startup Order.

Referensi #