Container Docker membungkus aplikasi Node beserta dependensinya menjadi satu citra (image) yang dapat dijalankan secara konsisten pada mesin apa pun. Pada konteks Node, praktik yang baik mencakup build multi-stage agar citra akhir hanya memuat berkas yang diperlukan saat runtime, pengguna non-root agar proses di dalam container tidak memiliki hak root, serta health check agar orkestrator seperti Kubernetes dapat mendeteksi container yang tidak sehat. Setelah membaca topik ini, pembaca dapat menulis Dockerfile multi-stage untuk aplikasi Node, mengonfigurasi .dockerignore, serta menambahkan HEALTHCHECK pada citra.
Build multi-stage#
Build multi-stage memakai beberapa tahap FROM pada satu Dockerfile, di mana setiap tahap memulai dari citra dasar baru dan menyalin hanya berkas yang diperlukan dari tahap sebelumnya. Tahap awal memuat perangkat build penuh yang dibutuhkan untuk mengompilasi TypeScript, merangkai berkas distribusi (bundling), dan membuang dependensi pengembangan. Tahap akhir hanya memuat runtime dan hasil build. Akibatnya, citra akhir lebih kecil dan tidak mengandung dependensi pengembangan yang memperbesar permukaan serangan, yaitu kumpulan komponen yang dapat menjadi target eksploitasi.
# tidak-dijalankan: Docker tidak tersedia di sandbox, bangun dengan `docker build -t catatan-suhu:1.0 .`
# tahap 1: build dependensi
FROM node:24-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
# tahap 2: build aplikasi
FROM node:24-bookworm-slim AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build
# tahap 3: runtime minimal
FROM node:24-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
# pengguna non-root
RUN groupadd --system nodeapp && useradd --system --gid nodeapp nodeapp
USER nodeapp
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:8080/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "dist/server.js"]Tahap deps menjalankan npm ci --omit=dev yang memasang hanya dependensi production dari package-lock.json, sehingga node_modules hasil salinan ke tahap runtime tidak memuat dev dependencies seperti ESLint atau TypeScript. Tahap builder memasang seluruh dependensi, menyalin kode sumber, dan menjalankan npm run build untuk menghasilkan direktori dist. Tahap runtime memulai dari citra dasar yang sama, menyalin node_modules dari tahap deps dan dist dari tahap builder, lalu menjalankan aplikasi.
Pemakaian npm ci alih-alih npm install membuat instalasi dapat direproduksi karena npm ci memasang versi yang persis tercatat pada package-lock.json tanpa memodifikasi berkas tersebut. Pada continuous integration, reproducibility ini penting agar citra yang dibangun dari komit yang sama selalu identik.
.dockerignore#
Berkas .dockerignore mendaftar berkas dan direktori yang dikecualikan saat docker build menyalin konteks build ke container. Tanpa berkas ini, COPY . . pada tahap builder akan menyalin node_modules lokal, berkas .git, dan berkas sementara lain, sehingga citra membesar dan build menjadi tidak deterministik karena node_modules lokal dapat berbeda dari yang seharusnya dipasang oleh npm ci.
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.env
.env.*
coverage
.nyc_output
dist
test
*.mdPengecualian node_modules lokal memakai npm ci menjadi sumber tunggal node_modules pada container. Pengecualian .env dan .env.* mencegah rahasia bocor ke citra, karena variabel lingkungan sebaiknya disuntik saat runtime lewat docker run --env-file atau orkestrator, bukan dibakar ke citra. Pengecualian test, coverage, dan *.md mengurangi ukuran citra karena berkas tersebut tidak diperlukan saat runtime.
Pengguna non-root#
Citra Node dasar secara default menjalankan proses sebagai root di dalam container, meskipun runtime Node sendiri tidak memerlukan hak root. Penjalanan sebagai root berbahaya karena kerentanan container escape dapat memberikan penyerang akses root pada host. Praktik yang baik adalah membuat pengguna sistem khusus dengan useradd --system, mengatur kepemilikan direktori aplikasi ke pengguna tersebut, lalu memasang direktif USER nodeapp sebelum CMD.
Pada Dockerfile di atas, perintah groupadd dan useradd membuat pengguna nodeapp yang tidak memiliki home directory maupun shell yang dapat dipakai untuk login. Direktif USER nodeapp membuat seluruh perintah setelahnya, termasuk CMD, berjalan sebagai pengguna tersebut. Citra Node resmi juga menyediakan pengguna bawaan bernama node dengan UID 1000, sehingga alternatif yang lebih ringkas adalah USER node tanpa membuat pengguna baru, asalkan kepemilikan berkas sudah diatur dengan benar.
HEALTHCHECK#
Direktif HEALTHCHECK menginstruksikan Docker untuk secara berkala menjalankan perintah yang memeriksa kesehatan container. Perintah tersebut mengembalikan kode keluar 0 bila sehat dan 1 bila tidak, lalu Docker menandai status container sebagai healthy atau unhealthy. Orkestrator seperti Kubernetes atau Docker Swarm dapat memakai status ini untuk me-restart container yang tidak sehat atau mengeluarkannya dari penyeimbang beban.
Pada Dockerfile di atas, HEALTHCHECK memakai node -e untuk memanggil fetch ke endpoint /healthz. Endpoint ini diharapkan mengembalikan respons HTTP 200 ketika aplikasi siap melayani, dan respons lain ketika tidak. Opsi --interval=30s mengatur jeda antar pemeriksaan, --timeout=3s membatasi durasi satu pemeriksaan, --start-period=5s memberi waktu grace saat container baru saja dimulai, dan --retries=3 menentukan berapa kali pemeriksaan gagal berturut-turut sebelum container ditandai unhealthy.
Kesalahan umum#
Kesalahan yang sering muncul adalah menjalankan npm install pada satu tahap Dockerfile tanpa pemisahan production dan dev dependencies. Akibatnya, citra akhir memuat ESLint, TypeScript, dan test runner, sehingga ukuran citra membengkak ratusan megabita dan permukaan serangan membesar. Perbaikannya adalah memakai npm ci --omit=dev pada tahap dependensi production dan menyalin hanya node_modules hasil prune tersebut ke tahap runtime.
Kesalahan lain adalah memasang package-lock.json sebagai COPY package*.json ./ tanpa npm ci. Pola COPY package*.json lalu npm install membuat build tidak dapat direproduksi karena npm install dapat memutakhirkan package-lock.json dan memasang versi yang berbeda. Pemakaian npm ci memastikan versi yang terpasang persis sama dengan yang tercatat pada lockfile, sehingga citra dari komit yang sama selalu identik.
Ringkasan#
- Build multi-stage memakai beberapa tahap
FROM, di mana tahap runtime hanya menyalinnode_moduleshasilnpm ci --omit=devdandisthasilnpm run build, sehingga citra akhir kecil dan tanpa dev dependencies. .dockerignoremengecualikannode_modules,.git,.env, dan berkas sementara lain dari konteks build, sehinggaCOPY . .tidak membawa berkas yang tidak diperlukan atau rahasia yang tidak boleh dibakar ke citra.- Direktif
USERmembuat proses dijalankan sebagai pengguna non-root, mengurangi risiko container escape yang memberikan akses root pada host. HEALTHCHECKmenjalankan perintah pemeriksaan berkala yang mengembalikan kode keluar 0 atau 1, sehingga Docker dan orkestrator dapat menandai container tidak sehat dan mengambil tindakan.
Referensi#
- Dockerfile reference — Docker
— Referensi resmi sintaks
DockerfilemencakupFROM,COPY --from,USER, danHEALTHCHECK. - Multi-stage builds — Docker
— Panduan resmi build multi-stage dan pemilihan tahap dengan
--target. - npm ci — npm
— Dokumentasi resmi
npm ciuntuk instalasi yang dapat direproduksi daripackage-lock.json.