PM2 menuntaskan satu masalah: menjaga proses tetap hidup di sebuah server. Masalah lain muncul ketika aplikasi harus berpindah — dari komputer kita ke server uji, lalu ke server produksi, atau saat rekan tim menyalakan proyek yang sama di sistem operasi berbeda. Kalimat “tapi di komputer saya jalan” adalah gejala klasiknya: versi Node.js berbeda, package native yang terkompilasi untuk arsitektur berbeda, atau berkas konfigurasi yang tersisa di satu mesin saja. Docker menjawab dengan mengemas aplikasi beserta seluruh kebutuhannya — runtime, dependencies, berkas — menjadi satu unit yang berjalan sama di mana pun. Pada chapter ini kita belajar menulis Dockerfile, menjaga build tetap cepat, dan menjalankan aplikasi Node.js di dalam container.
Konsep Inti: Image dan Container #
Dua kata ini menjadi fondasi seluruh bab Docker, jadi mari tegas bedanya. Image adalah cetakan baca-saja berisi semua yang dibutuhkan aplikasi: sistem berkas dasar, Node.js versi tertentu, node_modules, dan kode kita. Container adalah instance image yang sedang berjalan — sebuah proses di server kita, terisolasi dari proses lain, dengan berkas sistemnya sendiri. Hubungan keduanya serupa dengan class dan instance, atau resep masakan dengan masakan yang matang.
Image dibangun dari layer yang bertumpuk: setiap instruksi pada Dockerfile menghasilkan satu lapisan, dan lapisan yang tidak berubah bisa dipakai ulang dari cache. Sifat inilah yang nanti kita manfaatkan untuk mempercepat build. Konsep image, container, dan proses build-run dijelaskan lebih jauh pada panduan konsep Docker
.
Dockerfile untuk Aplikasi Node.js #
Dockerfile adalah resep pembuatan image — daftar instruksi yang dibaca dari atas ke bawah. Berikut bentuk yang layak dipakai sebagai titik awal untuk aplikasi Node.js kita:
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "src/server.mjs"]Mari kita bedah setiap instruksinya:
FROM node:24-slim: image dasar berisi Node.js versi 24 pada distribusi Debian yang ramping. Image resmi Node.js dipelihara oleh proyek docker-node ; memilih versi yang sama dengan lingkungan pengembangan menjaga perilaku runtime konsisten.WORKDIR /app: menetapkan direktori kerja untuk instruksi berikutnya — path relatif di dalam image kini merujuk ke/app.COPY package.json package-lock.json ./: hanya berkas metadata dependencies yang disalin pada tahap ini — bukan kode, bukannode_modules. Urutan ini disengaja, dan alasannya kita bahas di bagian berikutnya.RUN npm ci --omit=dev: memasang dependencies produksi persis seperti di Chapter Persiapan Deployment — dari lockfile, tanpa alat uji.COPY src ./src: kode aplikasi baru disalin setelah dependencies terpasang.ENV NODE_ENV=production: menetapkan environment variable di dalam image — berlaku untuk setiap container yang lahir darinya.USER node: menjalankan proses sebagai pengguna non-rootnodeyang sudah tersedia pada image resmi. Container yang berjalan sebagai root memberi permukaan galat yang lebih luas bila ada celah keamanan.EXPOSE 3000: dokumentasi bahwa aplikasi mendengarkan port 3000 — instruksi ini tidak menerbitkan port ke luar; pemetaan terjadi saatdocker run.CMD ["node", "src/server.mjs"]: perintah yang dijalankan saat container menyala, ditulis dalam bentuk exec form (array) agar sinyal sepertiSIGTERMsampai ke proses Node.js — persyaratan penting untuk graceful shutdown pada Chapter Graceful Shutdown.
Instruksi lain yang komplet terdokumentasi di referensi Dockerfile .
Urutan Instruksi Menentukan Kecepatan Build #
Setiap instruksi pada Dockerfile menghasilkan layer yang di-cache. Saat build berulang, Docker membandingkan setiap instruksi — bila isinya sama dengan cache, layer lama dipakai ulang dan seluruh instruksi setelahnya berhenti dievaluasi ulang sampai titik perubahan pertama. Di sinilah urutan pada Dockerfile di atas bekerja: COPY package.json package-lock.json ./ dan RUN npm ci berada di urutan awal, sehingga layer instalasi dependencies tetap terpakai selama kita hanya mengubah kode di src/ — npm ci yang tadinya berat tidak dijalankan ulang.
Konsekuensi kebalikannya juga berlaku: bila kita menulis COPY . ./ di urutan paling awal, setiap perubahan satu baris kode akan membatalkan cache layer npm ci. Urutan dari yang paling jarang berubah ke yang paling sering berubah adalah aturan praktisnya.
Satu keputusan penting lain: berkas node_modules dari komputer kita tidak ikut disalin — dan memang tidak boleh. Package berisi kode native dikompilasi untuk sistem operasi dan arsitektur tempat npm install berjalan; hasilnya di laptop macOS tidak selalu cocok untuk Debian di dalam image. Selain itu, mengirim node_modules sebagai bagian dari build context membuat proses build membawa ratusan megabyte yang tidak diperlukan.
Berkas .dockerignore #
Build context — folder yang kita kirim ke Docker saat menjalankan docker build — idealnya hanya berisi yang dibutuhkan image. Berkas .dockerignore di akar konteks membuang sisanya, persis seperti .gitignore untuk version control:
node_modules
.env
.git
npm-debug.log
testsDua baris pertama penting secara keamanan: node_modules dipastikan tidak tumpang-tindih dengan hasil npm ci di dalam image, dan berkas .env — tempat kita menulis secret lokal — tidak pernah ikut terkirim ke build context, apalagi tersimpan di dalam image. Mekanisme build context dan .dockerignore dijelaskan tuntas di konsep build context Docker
.
Memilih Varian Image Dasar #
Image resmi Node.js tersedia dalam beberapa varian dengan karakter berbeda:
| Varian | Contoh tag | Karakteristik |
|---|---|---|
| Lengkap | node:24 | berisi perkakas build lengkap; ukuran terbesar |
| Ramping Debian | node:24-slim | sistem dasar Debian yang dipangkas; pilihan seimbang |
| Alpine | node:24-alpine | paling ramping, sekitar 25% lebih kecil dari slim |
Varian slim adalah pilihan aman untuk memulai: cukup kecil, tetapi tetap memakai pustaka standar glibc sehingga package native umumnya langsung cocok. Varian alpine memakai musl sebagai pustaka C-nya; sebagian package native perlu dikompilasi ulang dan beberapa perkakas seperti git bahkan tidak terpasang bawaan. Ketiga karakteristik varian ini terdokumentasi pada README proyek docker-node yang tautannya sudah kita lihat di bagian Dockerfile.
Membangun dan Menjalankan #
Dengan Dockerfile dan .dockerignore di akar proyek, image dibangun lalu dijalankan dengan dua perintah inti:
docker build -t aplikasi-tugas:1.0 .
docker run -d --name tugas -p 3000:3000 --env-file .env aplikasi-tugas:1.0Perintah pertama membangun image dari build context (folder saat ini, tanda .) dan memberinya nama aplikasi-tugas berversi 1.0. Perintah kedua menjalankan container baru dari image tersebut dengan tiga opsi: -d menjalankannya di latar belakang, --name tugas memberi nama agar mudah dikelola, dan -p 3000:3000 memetakan port 3000 milik komputer ke port 3000 di dalam container — ingat bahwa EXPOSE hanya dokumentasi, pemetaan sungguhan terjadi di sini. Adapun --env-file .env menyuntikkan environment variable saat container menyala: secret tetap berada di luar image dan diberikan pada saat runtime.
Beberapa perintah pendamping melengkapi siklus hidupnya:
docker ps # daftar container yang berjalan
docker logs -f tugas # mengikuti aliran log aplikasi
docker stop tugas # mengirim SIGTERM lalu menunggu proses berhenti
docker rm tugas # menghapus container yang sudah berhentiPerintah docker stop layak dicermati: Docker mengirim SIGTERM ke proses utama dan memberi masa tenggang sebelum memaksa berhenti — mekanisme persis yang akan kita tangani di Chapter Graceful Shutdown. Itulah alasan CMD ditulis dalam exec form: agar sinyal sampai ke Node.js, bukan berhenti di shell perantara.
Satu container menjalankan satu aplikasi kita. Aplikasi nyata jarang berdiri sendirian — biasanya ada database, lalu mungkin proxy — dan mengelola banyak container dengan banyak docker run cepat menjadi repot. Docker Compose menyusun semuanya dalam satu berkas deklaratif — topik Chapter Docker Compose.