Menjalankan aplikasi di dalam container saat production sudah selesai adalah satu hal, tapi menjadikan container sebagai lingkungan kerja sehari-hari Developer adalah tantangan tersendiri. Kalau setiap perubahan baris kode harus diikuti docker build ulang dan docker run ulang, iterasi pengembangan jadi lambat dan justru bikin Docker terasa menghambat, bukan membantu. Bab ini membahas cara menyusun alur kerja pengembangan yang tetap cepat meski aplikasi berjalan di dalam container, mulai dari konfigurasi compose khusus development, teknik debugging proses di dalam container, mekanisme hot reload supaya perubahan kode langsung terlihat tanpa build ulang, sampai kebiasaan yang perlu dijaga supaya lingkungan development tetap konsisten dengan production.
41.1 Setup Development Environment dengan Docker Compose
Kebutuhan konfigurasi container saat development dan production hampir selalu berbeda. Saat development, kita ingin source code ter-mount langsung dari host supaya perubahan file instan terlihat di dalam container, port debugger ikut terbuka, dan tool build development (seperti nodemon atau watcher lain) aktif. Saat production, sebaliknya: image harus mandiri tanpa bergantung pada file host, tanpa port debugging yang terbuka, dan menjalankan proses production yang sudah dioptimalkan. Bagian ini membahas dua pendekatan resmi Docker Compose untuk memisahkan kebutuhan tersebut tanpa duplikasi konfigurasi.
41.1.1 Compose Override untuk Konfigurasi Development
Docker Compose mendukung penggabungan beberapa file konfigurasi sekaligus, di mana file yang disebut belakangan menimpa atau menambah definisi dari file sebelumnya. Secara default, kalau kita menjalankan docker compose tanpa opsi -f di direktori yang berisi compose.yaml dan compose.override.yaml sekaligus, Compose otomatis membaca dan menggabungkan keduanya. Pola ini pas dipakai untuk memisahkan konfigurasi inti (yang berlaku di semua environment) dari penyesuaian khusus development.
Siapkan compose.yaml sebagai definisi inti yang netral terhadap environment.
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:Lalu tambahkan compose.override.yaml yang berisi penyesuaian khusus development, seperti bind mount source code dan override perintah startup ke mode development.
services:
web:
volumes:
- ./src:/app/src
command: npm run dev
environment:
- NODE_ENV=developmentJalankan docker compose up seperti biasa tanpa opsi tambahan apa pun; Compose otomatis menggabungkan kedua file di atas untuk environment development. Saat deploy ke production, gunakan file override yang berbeda secara eksplisit lewat opsi -f, misalnya docker compose -f compose.yaml -f compose.production.yaml up -d, supaya konfigurasi development seperti bind mount source code tidak ikut terbawa ke production.
41.1.2 Sinkronisasi Kode Otomatis dengan Compose Watch
Bind mount sudah cukup untuk menyinkronkan file source code ke dalam container secara instan, tapi ada kasus yang tidak bisa ditangani bind mount saja, misalnya saat sebuah file perubahan (seperti package.json) semestinya memicu build ulang image, bukan sekadar sinkronisasi file. Docker Compose versi 2.22 ke atas menyediakan fitur Compose Watch lewat konfigurasi develop.watch untuk menangani skenario ini secara lebih presisi.
services:
web:
build: .
command: npm run dev
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- node_modules/
- action: rebuild
path: package.jsonKonfigurasi di atas memberi tahu Compose untuk melakukan dua hal berbeda tergantung file apa yang berubah. Perubahan pada direktori ./src (kecuali isi node_modules/) langsung disinkronkan ke /app/src di dalam container tanpa restart, sementara perubahan pada package.json memicu docker compose membangun ulang image secara otomatis karena biasanya berarti ada dependency baru yang perlu di-install. Jalankan mode watch lewat opsi --watch.
docker compose up --watchSelain sync dan rebuild, tersedia juga action sync+restart untuk kasus seperti perubahan file konfigurasi yang perlu disinkronkan lalu memicu restart container (tanpa build ulang image), cocok untuk file seperti konfigurasi nginx.conf yang hanya dibaca ulang saat proses restart. Dibanding bind mount biasa, Compose Watch lebih eksplisit soal action apa yang diinginkan per pola file, sehingga tim tidak perlu menerka-nerka mengapa satu perubahan file butuh restart sementara file lain tidak.
41.2 Debugging Container di Development
Bind mount dan hot reload menyelesaikan masalah kecepatan iterasi, tapi menjelajahi kondisi aplikasi saat masih dalam proses development tetap butuh teknik debugging tersendiri karena aplikasi berjalan terisolasi di dalam namespace container. Bagian ini membahas cara debugging cepat lewat perintah CLI Docker dan cara menyambungkan debugger interaktif dari editor ke proses yang berjalan di dalam container.
41.2.1 Debugging Cepat Lewat exec dan Logs
Untuk masalah yang tidak butuh breakpoint, kombinasi docker compose logs dan docker compose exec biasanya sudah cukup. Pantau log real-time dari satu service tertentu memakai opsi -f (follow) supaya output baru langsung tampil begitu muncul.
docker compose logs -f webKalau perlu memeriksa langsung kondisi di dalam container yang sedang berjalan, seperti isi environment variable yang benar-benar aktif atau isi file yang di-mount, gunakan docker compose exec untuk menjalankan perintah tanpa membuat container baru.
docker compose exec web shDi lapangan, kebiasaan mengecek log dan masuk ke dalam container lewat exec ini jauh lebih cepat dibanding langsung menuduh kode aplikasi bermasalah, terutama untuk kasus yang sebenarnya berasal dari konfigurasi environment variable yang salah atau file yang belum ter-mount dengan benar.
41.2.2 Attach Remote Debugger ke Proses di Dalam Container
Untuk debugging yang butuh breakpoint, step-through, dan inspeksi variable secara interaktif dari editor, proses di dalam container perlu dijalankan dengan mode debug yang membuka port khusus, lalu port tersebut dipublikasikan ke host supaya editor di luar container bisa menyambung. Runtime Node.js menyediakan flag --inspect bawaan untuk kebutuhan ini.
services:
web:
build: .
command: node --inspect=0.0.0.0:9229 src/index.js
ports:
- "3000:3000"
- "127.0.0.1:9229:9229"Ada dua alamat berbeda yang berperan di konfigurasi ini dan gampang tertukar. Alamat pada --inspect=0.0.0.0:9229 menentukan interface mana di dalam container yang didengarkan proses debugger; nilai ini wajib 0.0.0.0, bukan default 127.0.0.1 milik Node.js, karena publikasi port Docker meneruskan koneksi dari luar container dan koneksi tersebut hanya bisa diteruskan ke proses yang mendengarkan di semua interface, bukan cuma loopback internal container itu sendiri. Sebaliknya, alamat pada pemetaan port 127.0.0.1:9229:9229 menentukan interface host mana yang menerima publikasi port tersebut; membatasinya ke 127.0.0.1 memastikan port debugger hanya bisa diakses dari mesin host itu sendiri, bukan dari jaringan luar, sesuai praktik keamanan yang berlaku untuk port debugger bahasa apa pun. Setelah container berjalan, hubungkan editor (misalnya lewat konfigurasi "Attach to Node Process" di VS Code) ke localhost:9229.
Untuk aplikasi Python, pola yang sama berlaku memakai package debugpy dari Microsoft.
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.pyOpsi --wait-for-client membuat proses Python menunggu sampai debugger benar-benar terhubung sebelum mulai eksekusi, berguna untuk menangkap masalah yang terjadi sejak baris kode paling awal. Port 5678 ini dipetakan ke host dengan pola pemetaan yang sama seperti 9229 di atas, yaitu dibatasi ke 127.0.0.1. Terlepas dari bahasa yang dipakai, prinsipnya selalu sama: proses debugger di dalam container mendengarkan di semua interface (0.0.0.0) supaya bisa dijangkau publikasi port Docker, tapi publikasi port itu sendiri ke host dibatasi ke 127.0.0.1 supaya tidak terekspos ke jaringan luar, dan konfigurasi debug ini jangan pernah dibawa ke image production.
41.3 Hot Reloading dalam Container
Hot reload membuat perubahan source code langsung terlihat efeknya tanpa perlu restart manual, apalagi build ulang image. Prinsip dasarnya sederhana: source code disinkronkan ke dalam container lewat bind mount atau Compose Watch, lalu di dalam container berjalan tool watcher yang memantau perubahan file dan otomatis me-restart atau me-reload proses aplikasi begitu ada perubahan. Bagian ini membahas prinsip tersebut lebih dalam dan penerapannya untuk bahasa compiled seperti Go, yang punya kebutuhan berbeda dibanding bahasa interpreted.
41.3.1 Prinsip Hot Reload: Bind Mount dan Watcher Aplikasi
Hot reload di dalam container sebenarnya terdiri dari dua lapisan mekanisme terpisah yang sering tertukar pemahamannya. Lapisan pertama adalah sinkronisasi file dari host ke container, ditangani oleh bind mount atau Compose Watch seperti yang sudah dibahas sebelumnya. Lapisan kedua adalah watcher di level aplikasi, tool yang berjalan di dalam container dan memantau perubahan file untuk memicu reload proses, seperti nodemon untuk Node.js atau auto-reloader bawaan framework seperti Django dan Flask untuk Python.
Kedua lapisan ini harus sama-sama berfungsi supaya hot reload benar-benar bekerja. Bind mount saja tanpa watcher aplikasi hanya membuat file di dalam container ikut berubah tanpa ada yang memproses ulang aplikasinya; sebaliknya, watcher aplikasi tanpa bind mount tidak akan pernah mendeteksi perubahan apa pun karena file di dalam container memang tidak pernah berubah. Kesalahan paling umum di lapangan saat hot reload "tidak jalan" biasanya bukan salah satu dari dua hal ini yang gagal total, melainkan watcher aplikasi yang memakai mekanisme polling berbeda dari yang dibutuhkan filesystem ter-mount, terutama saat Docker dijalankan lewat Docker Desktop di macOS atau Windows dengan bind mount lintas filesystem host-VM. Kalau watcher berbasis event filesystem native (seperti inotify di Linux) tidak mendeteksi perubahan pada file yang di-mount lewat Docker Desktop, aktifkan mode polling eksplisit di konfigurasi watcher tersebut (misalnya CHOKIDAR_USEPOLLING=true untuk tool berbasis chokidar seperti yang dipakai banyak dev server JavaScript modern) sebagai solusi umum lintas platform.
41.3.2 Hot Reload untuk Bahasa Compiled
Bahasa compiled seperti Go tidak punya interpreter yang bisa langsung menjalankan ulang source code begitu berubah; source code harus dikompilasi ulang jadi binary terlebih dahulu sebelum bisa dijalankan. Tool watcher untuk bahasa compiled karena itu bekerja dengan pola berbeda: memantau perubahan file, memicu proses build ulang, lalu menjalankan ulang binary hasil build tersebut secara otomatis. Tool komunitas seperti Air menjalankan siklus watch-build-restart ini secara otomatis di dalam container development, dengan detail konfigurasi dan setup lengkap yang sudah dibahas pada bab studi kasus Golang.
Yang penting dipahami di level alur kerja adalah bahwa siklus watch-build-restart ini tetap lebih cepat dibanding melakukan docker build ulang setiap kali source code berubah, karena proses build binary Go berjalan langsung di dalam container yang sudah aktif (tanpa perlu membangun ulang image dari awal, termasuk layer dependency yang biasanya tidak berubah). Trade-off-nya, image development untuk bahasa compiled biasanya perlu menyertakan toolchain compiler penuh (bukan base image minimal seperti scratch atau distroless yang dipakai di production), karena kompilasi source code memang harus terjadi di dalam container tersebut.
41.4 Development Best Practices
Kecepatan iterasi yang didapat dari bind mount, Compose Watch, dan hot reload gampang berubah jadi masalah baru kalau lingkungan development justru makin jauh berbeda dari production, atau kalau kebiasaan development yang praktis ternyata membuka celah kalau tidak sengaja terbawa ke production. Tabel berikut merangkum kebiasaan yang perlu dijaga supaya alur kerja pengembangan tetap cepat tanpa mengorbankan konsistensi dan keamanan.
| Praktik | Alasan |
|---|---|
| Pakai base image yang sama antara development dan production, cukup beda target stage pada multi-stage build | Menghindari masalah "jalan di laptop, gagal di production" akibat perbedaan versi runtime atau OS base image |
Pisahkan file compose development (compose.override.yaml atau file kustom seperti compose.dev.yaml) dari file compose production secara eksplisit | Bind mount source code, port debugger, dan tool development lain tidak boleh ikut terbawa ke image atau konfigurasi production |
Jangan hardcode kredensial di compose.yaml, gunakan file .env yang masuk .gitignore | Kredensial development yang bocor ke repository sering dipakai ulang secara tidak sengaja di environment lain |
Saat memublikasikan port debugger (misalnya 9229 atau 5678) lewat opsi -p, batasi sisi host ke 127.0.0.1 meski proses debugger di dalam container tetap mendengarkan di 0.0.0.0 | Port debugger yang tidak diautentikasi dan terekspos ke jaringan luar bisa jadi jalan masuk Attacker untuk mengeksekusi kode secara remote |
Bersihkan container dan image development yang menumpuk secara berkala lewat docker compose down dan docker system prune; hapus volume lama secara sadar lewat opsi -v (bukan otomatis ikut terhapus), karena volume database development kadang masih dibutuhkan untuk debugging data | Environment development yang menumpuk container dan image lama sering jadi sumber bug palsu akibat state basi, sementara volume yang terhapus tanpa sengaja bisa menghilangkan data uji yang sulit dibuat ulang |
Prinsip yang mengikat semua praktik di atas sebenarnya sederhana: fleksibilitas untuk mempercepat iterasi selama development boleh dimanfaatkan seluas-luasnya, asalkan batas antara konfigurasi development dan production tetap jelas dan tidak pernah tertukar secara tidak sengaja. Alur kerja yang cepat tapi rapuh saat harus di-deploy ke production sebenarnya cuma memindahkan masalah, bukan menyelesaikannya.

