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:

KuncidevstagingproduksiKenapa harus beda
LOG_LEVELdebuginfowarndev butuh jejak lengkap; produksi akan tenggelam oleh log yang tidak dibaca siapa pun
WORKERS124laptop Budi dan server produksi tidak punya jumlah core yang sama
APP_ENVdevstagingproduksilabel identitas: perilaku kecil seperti pretty-print log mengikuti label ini
PAYMENT_CALLBACK_URLsandbox.example.com/pembayaransandbox.example.com/pembayaranapi.example.com/pembayaran/callbackstaging 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:

SumberBentuk deklarasiCocok untukBatasan
1 — environment variable runtimeenv_file / environment di compose.yamlnilai pendek yang berubah per environmentterbaca docker inspect — bukan tempat rahasia
2 — configs / bind mount :roconfigs dengan berkas dari projectkonfigurasi utuh: nginx.conf, templateisi mengikuti umur berkas, perlu disiplin versi
3 — ARG saat buildARG + --build-argmetadata build semata: nomor versi, commitnilai 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:

js
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:

  1. Daftar wajib eksplisit — kontrak konfigurasi aplikasi tertulis di satu tempat; siapa pun yang membaca server.js tahu persis kunci apa yang harus disiapkan deployment tanpa membaca dokumentasi terpisah.
  2. filter atas process.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.
  3. console.error, bukan console.log — pesan galat keluar melalui stderr dan tetap terbaca docker logs meski aplikasi mati sekejap setelah menyala.
  4. process.exit(1) — exit code 1 membuat kegagalan konfigurasi menjadi kejadian yang terlihat: docker compose ps -a menampilkan Exited (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:

bash
$ docker run --rm registry.example.com/toko/backend:1.0.0

Output yang Dihasilkan:

text
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:

yaml
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.conf

Berkas 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:

conf
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:

  1. image: dari registry, tanpa build: — deployment tidak mengompilasi apa pun; image ditaruh utuh dari registry, dan kebijakan tarik Compose bawaan missing cukup karena tag versi tidak pernah berpindah (callback Image, Build, dan Pull).
  2. env_file: env/${APP_ENV:-dev}.env — satu baris ini adalah seluruh mesin pemilih environment: APP_ENV diinterpolasi dari .env atau shell, dan default dev berarti lupa mengatur pemilih menjatuhkan stack ke environment paling aman, bukan paling mahal.
  3. configs untuk nginx.conf — konfigurasi proxy berstatus objek project, ter-review bersama compose.yaml, dan dipasang read-only tanpa path host yang tercecer.
  4. read_only: true + tmpfs untuk /tmp — filesystem backend dibekukan; aplikasi hanya boleh menggores area yang kita sediakan sengaja (resep Read-Only Filesystem).
  5. restart: unless-stopped dan stop_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.
  6. depends_on dengan service_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:

text
toko-produksi/
├── compose.yaml
├── nginx.conf
├── .env
└── env/
    ├── dev.env
    ├── staging.env
    └── produksi.env

Isi env/produksi.env — seluruhnya non-rahasia:

ini
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/callback

Sedangkan .env di akar project hanya memuat dua baris — pemilih environment dan satu nilai yang belum menemukan tempat yang layak:

ini
APP_ENV=produksi
DB_PASSWORD=rahasia123

Pemisahan 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:

bash
$ docker compose config

Output yang Dihasilkan (dipotong):

yaml
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:

bash
$ docker compose exec backend printenv | sort

Output yang Dihasilkan (dipotong):

text
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=4

Daftar 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 #