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:
| Service | Peran | Port host | Profil |
|---|---|---|---|
proxy | pintu tunggal: routing / dan /api/ | WEB_PORT, bawaan 8080 | — |
frontend | penyaji berkas statis toko | — | — |
backend | API Node.js dengan nodemon | — | — |
db | PostgreSQL 18 di volume db-data | — | — |
cache | Redis untuk cache dan antrean ringan | — | — |
migrate | job one-shot pembuat skema | — | — |
seed | job one-shot pengisi data contoh | — | debug |
adminer | alat admin web untuk database | 9090 | debug |
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:
toko/
├── api/
│ ├── Dockerfile
│ ├── package.json
│ ├── migrate.js
│ ├── seed.js
│ └── src/
│ └── server.js
├── frontend/
│ └── index.html
├── nginx/
│ └── nginx.conf
├── compose.yaml
├── compose.override.yaml
└── .env.exampleMari 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:
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:
base— dependensi produksi (npm ci --omit=dev) berikut seluruh berkas aplikasi; panggung ini dipakai langsung oleh jobmigratedanseedyang hanya membutuhkan pustakapg.dev—npm cidipasang ulang tanpa--omit=devsehingga nodemon ikut tersedia;CMDbawaannya menjalankan scriptdev. Perlu ditegaskan mengapa lewatnpm run: binary lokal terpasang dinode_modules/.binyang tidak berada diPATH— perintahnpm runmenyediakannya secara otomatis.runtime— panggung produksi:USER nodesesuai prinsip chapter Workdir dan User, danCMDtetap ditulis exec form — syarat sinyalSIGTERMsampai ke aplikasi yang kita buktikan di chapter Graceful Shutdown.
Script dev sendiri singkat — potongan package.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:
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_healthyMari kita telaah keputusan-keputusan penting pada lapisan itu:
buildditulis lengkap dengancontextdantarget: dev— mengulang context secara eksplisit membuat penggabungan override tidak bergantung pada penafsiran bentuk pendek; hasil gabungannya membangun panggung dev, bukan panggung terakhir.commandmenggantiCMDruntime dengannpm run dev— di lingkungan dev, backend berjalan di bawah nodemon yang memulai ulang proses setiap kode berubah.develop.watchdengan dua aturan — aksisyncmenyalin berkas yang berubah dari./api/srcke/app/srctanpa membangun ulang image, sedangkan aksirebuildmembangun ulang image dan container ketikapackage.jsonberubah — persis kosakata yang kita pelajari di chapter Compose Watch; berkas uji ikut diabaikan lewatignore.migratedanseedikut memakaitarget: dev— bukan karena butuh nodemon, melainkan agar backend, migrate, dan seed membangun konfigurasi yang sama persis: satu imagetoko-api:dev, satu proses build, tiga peran berbeda lewatcommand.seedbergantung padamigratedenganservice_completed_successfully— pola gerbang tiga tingkat dari chapter Startup dan Dependency: sebelum data contoh dimasukkan, skema harus sudah tercipta.adminerdi profildebugdengan port 9090 — port 8080 milik proxy, jadi alat admin web ini membuka pintu sendiri;ADMINER_DEFAULT_SERVER=dbmembuat 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:
proxy:
image: nginx:1.29-alpine
ports:
- "${WEB_PORT:-8080}:80"
configs:
- source: nginx-conf
target: /etc/nginx/conf.d/default.confBerkas configs pun mengikuti pindahan foldernya — file: ./nginx/nginx.conf. Nilai variabelnya hidup di .env, dan templatnya di-commit sebagai .env.example:
WEB_PORT=8080Interpolasi ${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:
git clone [email protected]:budi/toko.git
cd toko
cp .env.example .env
docker compose up -dOutput yang Dihasilkan (pembangunan image toko-api:dev dipotong):
[+] 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.8sSembilan 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:
docker compose watchOutput yang Dihasilkan (dipotong):
[+] 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:
docker compose logs -f backendOutput yang Dihasilkan (dipotong):
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:
docker compose up -d --buildOutput yang Dihasilkan (dipotong):
...
✔ Container toko-migrate-1 Exited 2.1s
✔ Container toko-backend-1 Recreated
✔ Container toko-backend-1 StartedPerhatikan 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:
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:
docker compose run --rm seedOutput yang Dihasilkan (dipotong):
[+] Running 2/2
✔ Container toko-db-1 Healthy
✔ Container toko-migrate-1 Exited
3 produk contoh dimasukkanTiga 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:
docker compose run --rm seedOutput yang Dihasilkan:
db sudah berisi data — seed dilewatiKebalikan dari seed adalah reset total — mengembalikan database ke hari pertama:
docker compose down -vOutput yang Dihasilkan:
[+] 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 RemovedBaris 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:
docker compose exec backend shOutput yang Dihasilkan:
/app $ node -v
v22.20.0
/app $ wget -qO- http://localhost:3000/healthz
ok
/app $ exitSesi 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:
docker compose logs -f proxy backendOutput yang Dihasilkan (dipotong):
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:
docker compose up -d adminerOutput yang Dihasilkan:
[+] Running 2/2
✔ Container toko-adminer-1 Started 0.9s
✔ Container toko-db-1 Running 0.0sSetelah 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.
Menutup Section: Stack yang Utuh #
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 #
- Use file watch mode
— panduan resmi aksi
sync,rebuild, dansync+restartpadadevelop.watch. - Referensi Compose File
— definisi kunci
develop,profiles, danconfigs. - Dokumentasi Docker Compose — ringkasan perilaku override dan penggabungan berkas compose.
- docker compose down — CLI Reference
— flag
-vdan cakupan pembersihan stack.