Image produksi yang kita rakit pada chapter sebelumnya punya satu sifat yang belum kita manfaatkan: isinya beku selamanya. registry.example.com/toko/backend:1.0.0 akan berjalan di development, di staging, dan di produksi — tiga tempat dengan kebutuhan yang berbeda — padahal byte demi byte isinya identik. Prinsip yang menyelamatkan kita dari kontradiksi itu adalah salah satu warisan paling awal dari metodologi 12-factor: konfigurasi hidup di environment, bukan di kode. Aplikasi yang sama, image yang sama, hanya disuntik nilai yang berbeda saat container diciptakan. Chapter ini menerapkan prinsip itu untuk toko: menyusun konfigurasi tiga environment dalam berkas yang bisa diverifikasi, mengajarkan backend menolak menyala ketika konfigurasinya timpang, lalu merakit compose.yaml produksi yang pertama.
Konfigurasi Adalah Environment, Bukan Kode #
Empat kunci berikut cukup untuk mewakili seluruh konfigurasi non-rahasia yang dibutuhkan backend toko — dan nilainya memang harus berbeda di tiga tempat:
| Kunci | dev | staging | produksi | Kenapa harus beda |
|---|---|---|---|---|
LOG_LEVEL | debug | info | warn | dev butuh jejak lengkap; produksi akan tenggelam oleh log yang tidak dibaca siapa pun |
WORKERS | 1 | 2 | 4 | laptop Budi dan server produksi tidak punya jumlah core yang sama |
APP_ENV | dev | staging | produksi | label identitas: perilaku kecil seperti pretty-print log mengikuti label ini |
PAYMENT_CALLBACK_URL | sandbox.example.com/pembayaran | sandbox.example.com/pembayaran | api.example.com/pembayaran/callback | staging membayar dengan uang mainan; hanya produksi yang boleh menyentuh penyedia pembayaran sungguhan |
Pembayaran adalah baris yang paling menentukan: satu environment salah isi, dan toko bisa mengirim notifikasi transaksi palsu ke penyedia pembayaran produksi, atau mengirim data kartu pelanggan sungguhan ke sandbox. Karena itu keputusan penempatan nilai tidak boleh didelegasikan ke kebiasaan.
Hierarki Tiga Sumber Konfigurasi #
Docker menyediakan tiga pintu tempat nilai bisa masuk, dan memilih pintu yang tepat adalah separuh keahlian mengelola konfigurasi. Urutan berikut disusun dari yang paling disukai sampai yang paling dihindari:
| Sumber | Bentuk deklarasi | Cocok untuk | Batasan |
|---|---|---|---|
| 1 — environment variable runtime | env_file / environment di compose.yaml | nilai pendek yang berubah per environment | terbaca docker inspect — bukan tempat rahasia |
2 — configs / bind mount :ro | configs dengan berkas dari project | konfigurasi utuh: nginx.conf, template | isi mengikuti umur berkas, perlu disiplin versi |
| 3 — ARG saat build | ARG + --build-arg | metadata build semata: nomor versi, commit | nilai terbakal ke layer dan terpampang di docker history |
Filosofinya satu kalimat: build-time menentukan identitas image, runtime menentukan kebijakan deployment. Nomor versi memang bagian dari jati diri image — pantas masuk LABEL lewat ARG. Alamat penyedia pembayaran adalah kebijakan lingkungan — pantas masuk env file. Aturan praktisnya: kalau nilai bisa berbeda antara dua mesin yang menjalankan image yang sama, nilai itu tidak boleh ditentukan saat build.
Fail-Fast saat Startup #
Konfigurasi yang hilang adalah kegagalan yang paling murah — asalkan aplikasi mengakuinya. Blok pembuka server.js 1.0.0 memuat validasi yang Budi tanam sebelum image dibekukan:
const wajib = [
"APP_ENV",
"LOG_LEVEL",
"WORKERS",
"DB_HOST",
"DB_USER",
"DB_NAME",
"REDIS_HOST",
];
const hilang = wajib.filter((kunci) => !process.env[kunci]);
if (hilang.length > 0) {
console.error("Konfigurasi tidak lengkap — kunci wajib belum tersedia:");
for (const kunci of hilang) {
console.error(` - ${kunci}`);
}
console.error("Periksa env file untuk environment ini sebelum menjalankan ulang.");
process.exit(1);
}Mari kita telaah empat keputusan pada blok tersebut:
- Daftar
wajibeksplisit — kontrak konfigurasi aplikasi tertulis di satu tempat; siapa pun yang membacaserver.jstahu persis kunci apa yang harus disiapkan deployment tanpa membaca dokumentasi terpisah. filteratasprocess.env— pemeriksaan berjalan sekali di awal tanpa pustaka validasi tambahan; blok ini sengaja memakai fitur bahasa paling polos supaya validasi itu sendiri tidak bisa gagal.console.error, bukanconsole.log— pesan galat keluar melalui stderr dan tetap terbacadocker logsmeski aplikasi mati sekejap setelah menyala.process.exit(1)— exit code 1 membuat kegagalan konfigurasi menjadi kejadian yang terlihat:docker compose ps -amenampilkanExited (1), daemon restart mencatatnya, dan tidak ada satu pun proses yang setengah hidup.
Perbedaannya paling terasa pada skenario yang pernah memakan waktu Budi dua puluh menit. Tanpa validasi, DB_HOST yang salah membuat backend tetap menyala dan mendengarkan port 3000; permintaan pertama ke /produk.json gagal dengan ECONNREFUSED yang terkubur di tengah jejak startup, dan investigasi menelusuri network policy, firewall, sampai healthcheck sebelum seseorang akhirnya membaca environment dengan teliti. Dengan validasi, container yang sama mati dalam dua detik sambil menyebut nama kunci yang hilang — dan memperbaikinya adalah pekerjaan satu baris. Kita bisa menyaksikan perilaku itu langsung pada image yang sudah jadi:
$ docker run --rm registry.example.com/toko/backend:1.0.0Output yang Dihasilkan:
Konfigurasi tidak lengkap — kunci wajib belum tersedia:
- APP_ENV
- LOG_LEVEL
- WORKERS
- DB_HOST
- DB_USER
- DB_NAME
- REDIS_HOST
Periksa env file untuk environment ini sebelum menjalankan ulang.compose.yaml Produksi #
Semua keputusan di atas bermuara pada satu berkas deployment. Budi membuat folder baru toko-produksi/ — terpisah dari repositori aplikasi, karena tugas folder ini bukan membangun image melainkan menjalankannya — dan menulis compose.yaml yang menarik image dari registry:
name: toko-produksi
services:
proxy:
image: nginx:1.29-alpine
ports:
- "8080:80"
networks:
- frontend-net
configs:
- source: nginx-conf
target: /etc/nginx/conf.d/default.conf
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
backend:
image: registry.example.com/toko/backend:1.0.0
networks:
- frontend-net
- backend-net
env_file:
- env/${APP_ENV:-dev}.env
environment:
DB_PASSWORD: ${DB_PASSWORD:?setel di berkas .env}
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stop_grace_period: 20s
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:18-alpine
networks:
- backend-net
environment:
POSTGRES_USER: toko
POSTGRES_PASSWORD: ${DB_PASSWORD:?setel di berkas .env}
POSTGRES_DB: toko
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
start_interval: 2s
restart: unless-stopped
cache:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
networks:
- backend-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
networks:
frontend-net:
backend-net:
volumes:
db-data:
configs:
nginx-conf:
file: ./nginx.confBerkas nginx.conf yang dirujuk deklarasi configs masih berupa router HTTP sementara — pola resolver + variabel yang dijanjikan Chapter Reverse Proxy akan menyusul bentuk penuhnya bersama sertifikat:
resolver 127.0.0.11 valid=10s;
server {
listen 80;
location /api/ {
set $backend_host backend:3000;
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://$backend_host;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
default_type text/plain;
return 503 "toko sedang naik kelas — lini depan TLS menyusul";
}
}Jalur /api/ sudah hidup, sedangkan / sengaja menjawab 503 karena frontend dan sertifikat akan naik bersama di Chapter Reverse Proxy dan TLS — rilis bertahap, pintu dicetak paling akhir. Database produksi menyala dengan volume db-data yang baru dan kosong; skema produk dimigrasikan oleh job one-shot pola Chapter Startup dan Dependency sebagai bagian rilis. Mari kita telaah enam kunci terpenting compose.yaml di atas:
image:dari registry, tanpabuild:— deployment tidak mengompilasi apa pun; image ditaruh utuh dari registry, dan kebijakan tarik Compose bawaanmissingcukup karena tag versi tidak pernah berpindah (callback Image, Build, dan Pull).env_file: env/${APP_ENV:-dev}.env— satu baris ini adalah seluruh mesin pemilih environment:APP_ENVdiinterpolasi dari.envatau shell, dan defaultdevberarti lupa mengatur pemilih menjatuhkan stack ke environment paling aman, bukan paling mahal.configsuntuknginx.conf— konfigurasi proxy berstatus objek project, ter-review bersama compose.yaml, dan dipasang read-only tanpa path host yang tercecer.read_only: true+tmpfsuntuk/tmp— filesystem backend dibekukan; aplikasi hanya boleh menggores area yang kita sediakan sengaja (resep Read-Only Filesystem).restart: unless-stoppeddanstop_grace_period: 20s— container bangkit sendiri setelah crash tanpa membatalkan keputusan operator berhenti, dan diberi dua puluh detik upacara penutupan yang sudah dibuktikan habis oleh Chapter Graceful Shutdown.depends_ondenganservice_healthy— backend hanya menyala setelah db dan cache lulus probe; proxy menyusul setelah backend sehat — rantai kesiapan yang sama dengan stack pengembangan, kini untuk pengguna sungguhan.
Struktur Folder Tiga Environment #
Berkas-berkas env hidup di subfolder env/, dipisahkan per environment, dengan .env kecil di akar sebagai papan kendali interpolasi:
toko-produksi/
├── compose.yaml
├── nginx.conf
├── .env
└── env/
├── dev.env
├── staging.env
└── produksi.envIsi env/produksi.env — seluruhnya non-rahasia:
APP_ENV=produksi
LOG_LEVEL=warn
WORKERS=4
DB_HOST=db
DB_USER=toko
DB_NAME=toko
REDIS_HOST=cache
PAYMENT_CALLBACK_URL=https://api.example.com/pembayaran/callbackSedangkan .env di akar project hanya memuat dua baris — pemilih environment dan satu nilai yang belum menemukan tempat yang layak:
APP_ENV=produksi
DB_PASSWORD=rahasia123Pemisahan ini bukan gaya penulisan, melainkan garis batas kelas barang. Semua kunci di env/*.env boleh diperlihatkan kepada siapa pun yang boleh membaca compose.yaml — host database internal, nama user, tingkat log. Satu-satunya penghuni yang tidak enak dipandang adalah DB_PASSWORD: ia berpindah sebagai teks polos, diinterpolasi ke environment container, dan karenanya akan terbaca oleh docker inspect — kelakuan yang sudah kita buktikan pada Chapter Secret Management. Untuk satu bab ini ia kita biarkan menumpang, sambil dicatat besar-besaran sebagai utang.
Verifikasi Konfigurasi yang Terpasang #
Ritus verifikasi dimulai sebelum stack menyala. docker compose config merender seluruh interpolasi dan resolusi env file, sehingga kesalahan pemilih environment atau kunci yang kosong ketahuan tanpa menjalankan satu container pun:
$ docker compose configOutput yang Dihasilkan (dipotong):
name: toko-produksi
services:
backend:
env_file:
- env/produksi.env
environment:
DB_PASSWORD: rahasia123
image: registry.example.com/toko/backend:1.0.0
...Render tersebut menjawab dua hal sekaligus: env/produksi.env terpilih karena APP_ENV terbaca dari .env, dan DB_PASSWORD terisi karena berkas yang sama menyediakannya. Stack kemudian dinyalakan dengan docker compose up -d, gerbang-gerbang service_healthy terlewati satu per satu, dan konfigurasi yang benar-benar menempel pada container diperiksa dari dalam:
$ docker compose exec backend printenv | sortOutput yang Dihasilkan (dipotong):
APP_ENV=produksi
DB_HOST=db
DB_NAME=toko
DB_PASSWORD=rahasia123
DB_USER=toko
...
LOG_LEVEL=warn
NODE_ENV=production
...
REDIS_HOST=cache
WORKERS=4Daftar itu membuktikan seluruh rantai bekerja: env file terbaca, nilai produksi menempel, dan NODE_ENV=production dari image berdampingan dengan konfigurasi runtime. Tetapi Budi berhenti lama pada satu baris — DB_PASSWORD=rahasia123 terpampang polos di environment container, tepat kelakuan yang di Section Keamanan Docker kita buktikan bisa dibaca siapa pun yang memegang akses Docker lewat docker inspect. Utang yang tadi dicatat besar-besaran kini jatuh tempo: password database tidak boleh terus hidup sebagai environment variable. Compose menyediakan mekanisme yang lebih layak — berkas rahasia yang terpasang read-only di /run/secrets — dan itulah materi Chapter Secrets dan Environment.
Referensi #
- Environment files
— format berkas
.env, aturan barisKUNCI=nilai, dan pemakaiannya lewatenv_file. - Variable interpolation
— pola
${VAR:-default}dan${VAR:?pesan}beserta urutan sumber nilainya. - Compose File Reference — Services
— definisi kunci
env_file,environment,configs, dandepends_on. - docker compose config — CLI Reference — validasi dan penampilan konfigurasi hasil resolusi.