Sepanjang section ini kita menumbuhkan aplikasi toko satu pola demi satu pola: arsitektur multi service, reverse proxy sebagai pintu tunggal, aplikasi stateless dengan database ber-volume, cache dan antrean, resolusi nama lewat DNS Compose, urutan start yang bertingkat, hingga penghentian yang sopan. Chapter penutup ini merangkai seluruhnya menjadi satu stack development harian yang utuh: satu repositori, satu rangkaian perintah docker compose, dan seluruh lingkungan kerja — proxy, database, cache, migrasi, data contoh, sampai alat administrasi — hidup di laptop siapa pun yang ikut proyek dalam hitungan menit. Prinsipnya bukan sekadar mempermudah hidup developer, melainkan membuat lingkungan dev menyerupai production: proxy, network, urutan start, dan perilaku shutdown yang diuji setiap hari di laptop adalah hal yang sama yang akan berjalan di server, sehingga bug yang lahir dari interaksi antar service bisa direproduksi lokal sebelum sempat terbang ke production.

Dev yang Menyerupai Production #

Stack development yang baik memuat service yang sama dengan rencana production — bukan versi ringkasnya. Yang boleh berbeda hanyalah hal-hal yang memang khas dev: kode tersinkron tanpa rebuild, server aplikasi menyala ulang sendiri saat kode berubah, database berisi data contoh yang siap pakai, dan alat bantu debug tersedia tapi tidak mengganggu. Susunan lengkap stack dev toko setelah chapter ini selesai tampak seperti ini:

ServicePeranPort hostProfil
proxypintu tunggal: routing / dan /api/WEB_PORT, bawaan 8080—
frontendpenyaji berkas statis toko——
backendAPI Node.js dengan nodemon——
dbPostgreSQL 18 di volume db-data——
cacheRedis untuk cache dan antrean ringan——
migratejob one-shot pembuat skema——
seedjob one-shot pengisi data contoh—debug
admineralat admin web untuk database9090debug

Kolom Profil adalah gerbang kedisiplinan: seed dan adminer sengaja dipasang di profil debug — pola dari chapter Profiles — sehingga keduanya hanya ikut menyala saat diminta, dan stack harian tetap ramping dengan enam service inti yang sama seperti production.

Struktur Proyek Final #

Seluruh kebutuhan stack hidup dalam satu repositori. Sebelum merakit, kita rapikan dulu tata letak toko dengan tiga perubian kecil: server.js pindah ke api/src/ agar kode terpisah rapi dari berkas tugas; nginx.conf pindah ke folder nginx/; dan satu berkas baru compose.override.yaml berdiri di samping compose.yaml:

text
toko/
├── api/
│   ├── Dockerfile
│   ├── package.json
│   ├── migrate.js
│   ├── seed.js
│   └── src/
│       └── server.js
├── frontend/
│   └── index.html
├── nginx/
│   └── nginx.conf
├── compose.yaml
├── compose.override.yaml
└── .env.example

Mari kita telaah peran tiap sudutnya: api/ memuat Dockerfile, skrip tugas, dan source aplikasi backend; frontend/ memuat berkas statis yang di-bind mount oleh service frontend; nginx/ memuat konfigurasi proxy yang dipasang lewat configs; compose.yaml adalah definisi stack yang berlaku untuk semua lingkungan; compose.override.yaml adalah lapisan khusus dev yang digabung Compose secara otomatis — mekanisme merge yang kita bedah di chapter Compose Override dan Environment. Untuk proyek latihan ini override ikut di-commit sebagai konfigurasi dev bersama; di tim yang lebih besar polanya bebas berbeda — override dibiarkan tidak di-commit dan tiap orang menyusun miliknya sendiri, atau memakai berkas eksplisit compose.dev.yaml lewat flag -f. Berkas .env.example adalah templat nilai yang bervariasi per orang, sedangkan .env hasil salinannya tidak di-commit.

Dockerfile dengan Panggung dev #

Image produksi dan image dev berbeda kebutuhan: produksi cukup dependensi runtime dan berjalan sebagai user biasa, sedangkan dev butuh seluruh devDependencies — termasuk nodemon. Multi-stage build dari chapter Multi-Stage Build menyelesaikannya dalam satu Dockerfile:

dockerfile
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY migrate.js seed.js ./
COPY src ./src

FROM base AS dev
RUN npm ci
CMD ["npm", "run", "dev"]

FROM base AS runtime
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]

Mari kita telaah tiga panggung tersebut:

  1. base — dependensi produksi (npm ci --omit=dev) berikut seluruh berkas aplikasi; panggung ini dipakai langsung oleh job migrate dan seed yang hanya membutuhkan pustaka pg.
  2. dev — npm ci dipasang ulang tanpa --omit=dev sehingga nodemon ikut tersedia; CMD bawaannya menjalankan script dev. Perlu ditegaskan mengapa lewat npm run: binary lokal terpasang di node_modules/.bin yang tidak berada di PATH — perintah npm run menyediakannya secara otomatis.
  3. runtime — panggung produksi: USER node sesuai prinsip chapter Workdir dan User, dan CMD tetap ditulis exec form — syarat sinyal SIGTERM sampai ke aplikasi yang kita buktikan di chapter Graceful Shutdown.

Script dev sendiri singkat — potongan package.json:

json
{
  "type": "module",
  "scripts": {
    "dev": "nodemon src/server.js"
  },
  "devDependencies": {
    "nodemon": "^3"
  }
}

Panggung dev ini kita sengaja jalankan tanpa flag --inspect — debugging jarak jauh dari editor adalah pembahasan tersendiri di chapter Debugging Container pada section Docker dalam Pengembangan.

compose.override.yaml: Lapisan Dev #

Seluruh penyesuaian dev kini hidup di satu berkas yang digabung otomatis ke compose.yaml:

yaml
services:
  backend:
    build:
      context: ./api
      target: dev
    command: ["npm", "run", "dev"]
    develop:
      watch:
        - action: sync
          path: ./api/src
          target: /app/src
          ignore:
            - "*.test.js"
        - action: rebuild
          path: ./api/package.json

  migrate:
    build:
      context: ./api
      target: dev

  seed:
    build:
      context: ./api
      target: dev
    command: ["node", "seed.js"]
    environment:
      - DATABASE_URL=postgres://toko:rahasia123@db-utama:5432/toko
    networks:
      - backend-net
    depends_on:
      migrate:
        condition: service_completed_successfully
    profiles: ["debug"]

  adminer:
    image: adminer:5
    profiles: ["debug"]
    ports:
      - "9090:8080"
    environment:
      - ADMINER_DEFAULT_SERVER=db
    networks:
      - backend-net
    depends_on:
      db:
        condition: service_healthy

Mari kita telaah keputusan-keputusan penting pada lapisan itu:

  1. build ditulis lengkap dengan context dan target: dev — mengulang context secara eksplisit membuat penggabungan override tidak bergantung pada penafsiran bentuk pendek; hasil gabungannya membangun panggung dev, bukan panggung terakhir.
  2. command mengganti CMD runtime dengan npm run dev — di lingkungan dev, backend berjalan di bawah nodemon yang memulai ulang proses setiap kode berubah.
  3. develop.watch dengan dua aturan — aksi sync menyalin berkas yang berubah dari ./api/src ke /app/src tanpa membangun ulang image, sedangkan aksi rebuild membangun ulang image dan container ketika package.json berubah — persis kosakata yang kita pelajari di chapter Compose Watch; berkas uji ikut diabaikan lewat ignore.
  4. migrate dan seed ikut memakai target: dev — bukan karena butuh nodemon, melainkan agar backend, migrate, dan seed membangun konfigurasi yang sama persis: satu image toko-api:dev, satu proses build, tiga peran berbeda lewat command.
  5. seed bergantung pada migrate dengan service_completed_successfully — pola gerbang tiga tingkat dari chapter Startup dan Dependency: sebelum data contoh dimasukkan, skema harus sudah tercipta.
  6. adminer di profil debug dengan port 9090 — port 8080 milik proxy, jadi alat admin web ini membuka pintu sendiri; ADMINER_DEFAULT_SERVER=db membuat kolom server di halaman loginnya sudah terisi.

Nilai per Orang di .env #

Satu perubahan kecil terakhir di compose.yaml: port proxy kita naikkan dari angka mati menjadi variabel dengan nilai bawaan:

yaml
  proxy:
    image: nginx:1.29-alpine
    ports:
      - "${WEB_PORT:-8080}:80"
    configs:
      - source: nginx-conf
        target: /etc/nginx/conf.d/default.conf

Berkas configs pun mengikuti pindahan foldernya — file: ./nginx/nginx.conf. Nilai variabelnya hidup di .env, dan templatnya di-commit sebagai .env.example:

ini
WEB_PORT=8080

Interpolasi ${VAR:-bawaan} adalah kosakata dari chapter Variable Interpolation: mesin tanpa .env tetap memperoleh 8080, sedangkan mesin yang butuh berbeda cukup mengganti satu baris di .env miliknya.

Alur Hari Kerja Developer #

Ujian sesungguhnya dari stack ini adalah hari pertama Siti bergabung. Seluruh langkahnya pendek:

bash
git clone [email protected]:budi/toko.git
cd toko
cp .env.example .env
docker compose up -d

Output yang Dihasilkan (pembangunan image toko-api:dev dipotong):

text
[+] Running 9/9
 ✔ Network toko_frontend-net    Created   0.1s
 ✔ Network toko_backend-net     Created   0.1s
 ✔ Volume "toko_db-data"        Created   0.0s
 ✔ Container toko-cache-1       Healthy   4.1s
 ✔ Container toko-db-1          Healthy   12.2s
 ✔ Container toko-migrate-1     Exited    2.4s
 ✔ Container toko-backend-1     Started   0.6s
 ✔ Container toko-frontend-1    Started   0.4s
 ✔ Container toko-proxy-1       Started   6.8s

Sembilan baris itu adalah rantai urutan hidup yang kita bangun di chapter Startup dan Dependency, kini bekerja tanpa kita menyentuh apa pun: db dan cache menunggu healthcheck, migrasi menunggu db sehat, backend menunggu ketiganya, proxy menunggu backend sehat. Lalu sesi pemantauan dinyalakan di terminal tersendiri:

bash
docker compose watch

Output yang Dihasilkan (dipotong):

text
[+] Running 6/6
 ✔ Container toko-cache-1       Running   0.0s
 ✔ Container toko-db-1          Running   0.0s
 ...

Setelah baris-baris tersebut terminal duduk tenang di foreground memantau perubahan berkas — sesi ini ditutup dengan Ctrl+C tanpa mematikan stack, perilaku yang kita kenal dari chapter Compose Watch. Hari kerja selanjutnya berjalan dalam lingkaran kecil: Siti menyimpan suntingan di frontend/index.html dan cukup menyegarkan browser karena bind mount menyajikan berkas langsung; ia menyimpan suntingan di api/src/server.js dan watch menyinkronkannya seketika:

bash
docker compose logs -f backend

Output yang Dihasilkan (dipotong):

text
toko-backend-1  | [nodemon] restarting due to changes...
toko-backend-1  | [nodemon] starting `node src/server.js`

Dua baris itu adalah seluruh jarak antara “menyimpan berkas di editor” dan “perubahan hidup di dalam container” — sync menyalin berkasnya, nodemon memulai ulang prosesnya. Ketika perubahan menyentuh package.json, aturan rebuild mengurus build ulang secara otomatis; dan untuk perubahan yang berada di luar jangkauan pemantauan — misalnya commit rekan yang mengubah Dockerfile setelah git pull — perintah manual penuhnya:

bash
docker compose up -d --build

Output yang Dihasilkan (dipotong):

text
...
 ✔ Container toko-migrate-1   Exited    2.1s
 ✔ Container toko-backend-1   Recreated
 ✔ Container toko-backend-1   Started

Perhatikan baris migrate pada keluaran itu: image yang dibangun ulang membuat container job dibuat ulang, sehingga migrasi berjalan sekali lagi — dan di sinilah idempotensi yang kita wajibkan di chapter Startup dan Dependency membayar cicilannya: CREATE TABLE IF NOT EXISTS menjadikan pengulangan itu membosankan, bukan berbahaya.

Data Contoh: seed dan ritual reset #

Skema dan data contoh adalah dua urusan berbeda — skema wajib hadir di production, sedangkan data contoh hanya milik dev. Karena itu baris INSERT data awal yang dulu ikut di migrate.js kita pindahkan ke seed.js, dengan penjagaan agar ia hanya bekerja saat tabel masih kosong:

js
import pg from "pg";

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

const { rows } = await pool.query("SELECT COUNT(*) AS jumlah FROM produk");

if (rows[0].jumlah > 0) {
  console.log("db sudah berisi data — seed dilewati");
  await pool.end();
  process.exit(0);
}

await pool.query(`
  INSERT INTO produk (nama, harga) VALUES
    ('Kopi Toraja', 45000),
    ('Teh Melati', 18000),
    ('Gula Aren Cair', 25000)
`);

console.log("3 produk contoh dimasukkan");
await pool.end();

Mari kita telaah alurnya: hitung dulu isi tabel produk; bila sudah ada baris, seed menyapa singkat lalu keluar exit 0 — idempoten dan tidak pernah menduplikasi; bila kosong, tiga produk contoh dimasukkan sekaligus. Pengisiannya adalah ritual satu perintah:

bash
docker compose run --rm seed

Output yang Dihasilkan (dipotong):

text
[+] Running 2/2
 ✔ Container toko-db-1        Healthy
 ✔ Container toko-migrate-1   Exited
3 produk contoh dimasukkan

Tiga hal bekerja sekaligus pada perintah itu: profil debug aktif otomatis karena seed disebut eksplisit sebagai target — aturan aktivasi dari chapter *Profiles; gerbang dependensinya dihormati (db sehat, migrasi tuntas); dan –rm` membersihkan container job begitu ia selesai. Menjalankannya ulang pun aman:

bash
docker compose run --rm seed

Output yang Dihasilkan:

text
db sudah berisi data — seed dilewati

Kebalikan dari seed adalah reset total — mengembalikan database ke hari pertama:

bash
docker compose down -v

Output yang Dihasilkan:

text
[+] Running 9/9
 ✔ Container toko-proxy-1     Removed
 ✔ Container toko-frontend-1  Removed
 ✔ Container toko-backend-1   Removed
 ✔ Container toko-migrate-1   Removed
 ✔ Container toko-cache-1     Removed
 ✔ Container toko-db-1        Removed
 ✔ Network toko_frontend-net  Removed
 ✔ Network toko_backend-net   Removed
 ✔ Volume toko_db-data        Removed

Baris terakhir itulah intinya: flag -v ikut menghapus volume db-data, sehingga seluruh data — pesanan, produk, uji coba — lenyap permanen. down -v adalah tombol reset total, bukan bagian ritme harian; setelah menekannya, kembalikan stack dengan docker compose up -d lalu docker compose run --rm seed, dan toko kembali lahir dari nol lengkap dengan data contoh.

Debug di Dalam Stack #

Ketika perilaku aneh muncul, tiga pintu masuk menunggu. Pintu pertama: masuk ke dalam container itu sendiri:

bash
docker compose exec backend sh

Output yang Dihasilkan:

text
/app $ node -v
v22.20.0
/app $ wget -qO- http://localhost:3000/healthz
ok
/app $ exit

Sesi shell tersebut menjalankan perintah seolah kita duduk di dalam container — memeriksa versi runtime, mengetuk endpoint dari balik tembok network, atau membaca berkas yang tersinkron lewat watch; pola exec ini kita kenal sejak chapter Menjalankan Perintah dalam Container.

Pintu kedua: membaca arus log beberapa service sekaligus, dengan prefiks nama container sebagai pembeda:

bash
docker compose logs -f proxy backend

Output yang Dihasilkan (dipotong):

text
toko-proxy-1   | 172.21.0.1 - - [14/Oct/2026:11:20:03 +0000] "GET /api/produk.json HTTP/1.1" 200 62 "-" "curl/8.14.1"
toko-backend-1 | [nodemon] restarting due to changes...

Dua prefiks berdampingan memudahkan kita menyusun kronologi lintas service: permintaan masuk lewat proxy, proses backend menyala ulang karena suntingan. Pintu ketiga: mengintip isi database dari antarmuka web — alat admin ringan mis. Adminer — yang tinggal dinyalakan:

bash
docker compose up -d adminer

Output yang Dihasilkan:

text
[+] Running 2/2
 ✔ Container toko-adminer-1  Started   0.9s
 ✔ Container toko-db-1       Running   0.0s

Setelah itu http://localhost:9090 terbuka: login dengan toko / rahasia123 ke server db, dan seluruh tabel toko bisa dijelajahi, disunting, bahkan dijadikan tempat berlatih kueri. Sebelum melanjutkan pekerjaan, alat ini bisa dibiarkan — ia hanya hidup saat profil debug diaktifkan — atau dimatikan kembali dengan docker compose rm -sf adminer agar stack harian kembali ramping.

Perjalanan section ini tuntas dengan urutan yang rapi. Kita mulai dari Arsitektur Multi-Container — alasan memecah aplikasi menjadi service dan garis batas stateless versus stateful; Reverse Proxy menyatukan pintu masuk di satu port dengan routing path; Application dan Database menghubungkan aplikasi stateless ke PostgreSQL bervolume dengan connection pool dan migrasi; Cache dan Queue mempercepat pembacaan dan menenangkan penulisan; Service Discovery menjelaskan bagaimana nama-nama sederhana terselesaikan dan bagaimana klien yang tangguh menunggunya; Startup dan Dependency mengatur urutan hidup bertingkat dengan healthcheck dan job one-shot; Graceful Shutdown memastikan stack berhenti tanpa meninggalkan korban; dan chapter ini merakit semuanya menjadi lingkungan kerja harian yang menyala dalam satu perintah. Satu komponen kini tampak semakin berurat akar dalam segala kisah itu: database — volumenya, inisialisasinya, migrasinya, kesehatannya, sampai kebiasaannya saat dimatikan. Section berikutnya, Docker dan Database, membedah komponen itu tuntas satu chapter demi satu chapter, dimulai dari Menjalankan PostgreSQL.

Referensi #