Image Docker yang besar dan lambat membangun sering kali menjadi bottleneck dalam CI/CD pipeline. Multi-stage build adalah teknik optimasi yang memungkinkan kamu memisahkan environment build dan runtime, sehingga image final hanya berisi artefak yang benar-benar diperlukan. Artikel ini akan membahas cara mengimplementasikan multi-stage build untuk aplikasi Node.js dengan praktik terbaik.
Secara tradisional, Dockerfile sering menggabungkan langkah instalasi dependency, kompilasi, dan runtime dalam satu stage. Hasilnya image berisi toolchain build, cache package, dan file sumber yang tidak dibutuhkan saat production. Ukuran image bisa membengkak hingga ratusan megabyte bahkan gigabyte.
Dengan multi-stage build, kamu mendefinisikan beberapa stage di dalam satu Dockerfile. Stage pertama menangani build dan instalasi. Stage kedua menyalin hanya hasil build ke image runtime yang lebih minimal. Ini secara drastis mengurangi ukuran dan attack surface.
Keuntungan lain adalah caching yang lebih efisien. Stage yang tidak berubah bisa menggunakan cache dari build sebelumnya. Ini mempercepat iterasi development dan mengurangi penggunaan bandwidth registry.
Sebelum memulai, pastikan kamu memiliki:
Docker Engine 17.05 atau lebih baru (multi-stage build diperkenalkan di versi ini)
Aplikasi Node.js dengan package.json dan entry point yang jelas
Pemahaman dasar tentang Dockerfile syntax dan layer caching
Akses ke Docker Hub atau private registry untuk push image final
Untuk memahami perbedaan, mari kita mulai dengan Dockerfile single stage yang umum digunakan. Buat file bernama Dockerfile di root project dengan isi:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]
Build image ini dan periksa ukurannya:
docker build -t myapp:single .
docker images myapp:single --format "{{.Size}}"
Kemungkinan besar ukuran akan di atas 300 MB karena image node:20-alpine mengandung package manager dan cache. Catat ukuran ini sebagai baseline untuk perbandingan nanti.
Modifikasi Dockerfile untuk menggunakan dua stage. Stage pertama dinamai build dan menangani instalasi dependency serta kompilasi. Stage kedua hanya menyalin artefak yang diperlukan.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]
Perintah COPY --from=build menyalin file dari stage build ke stage runtime. Perhatikan bahwa kita juga menyalin node_modules karena aplikasi runtime masih memerlukan dependency production.
Masih ada ruang untuk optimasi. Stage runtime tidak perlu devDependencies seperti test runner, linter, atau type definition. Gunakan perintah npm ci --omit=dev di stage runtime untuk mengurangi ukuran node_modules.
Modifikasi Dockerfile:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]
Dengan perubahan ini, node_modules di stage runtime hanya berisi package yang diperlukan saat production. Ukuran bisa berkurang 30 hingga 50 persen tergantung jumlah devDependencies.
Untuk pengurangan ukuran maksimal, gunakan image base yang lebih kecil seperti distroless atau alpine versi minimal. Distroless image dari Google hanya berisi runtime tanpa shell, package manager, atau utilitas sistem.
Contoh dengan distroless:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["dist/main.js"]
Perhatikan bahwa distroless tidak memiliki shell. Jika kamu memerlukan debugging di container, pertimbangkan untuk menggunakan alpine minimal sebagai alternatif. Keamanan distroless lebih tinggi karena attack surface lebih kecil.
Urutan instruksi di Dockerfile sangat mempengaruhi caching. Dependency yang jarang berubah harus diletakkan di awal, sedangkan source code yang sering berubah di akhir. Ini memastikan layer dependency tidak perlu di-build ulang setiap kali ada perubahan kode.
Strategi optimal untuk Node.js:
1. COPY package*.json
2. RUN npm ci
3. COPY . .
4. RUN npm run build
Jika package.json tidak berubah, Docker akan menggunakan cache untuk layer npm ci. Ini menghemat waktu build secara signifikan, terutama untuk project dengan dependency yang banyak.
Gunakan juga .dockerignore untuk mengecualikan file yang tidak perlu masuk ke context build. Contoh: node_modules, .git, logs, dan file environment lokal.
Setelah mengoptimasi Dockerfile, build image multi-stage dan bandingkan dengan baseline:
docker build -t myapp:multi .
docker images myapp --format "table {{.Tag}} {{.Size}}"
Selain ukuran, periksa juga waktu build dan keberhasilan startup. Jalankan container untuk memastikan aplikasi berjalan normal:
docker run -p 3000:3000 myapp:multi
Lakukan benchmark load dengan tools seperti Artillery atau k6 untuk memastikan performa runtime tidak terpengaruh oleh perubahan image base. Kadang image minimal mengakibatkan masalah DNS atau TLS yang perlu diperhatikan.
Multi-stage build adalah teknik fundamental untuk optimasi Docker image. Dengan memisahkan stage build dan runtime, menggunakan dependency production, dan memilih image base yang tepat, kamu bisa mengurangi ukuran image drastis sambil meningkatkan keamanan. Praktik ini juga mempercepat deployment dan mengurangi penggunaan storage di registry.
Untuk referensi lebih lanjut, kunjungi dokumentasi resmi Docker Multi-Stage Build.
Dapatkan feedback, users, dan eksposur dari komunitas kreator, developer, dan entrepreneur digital Indonesia.
Submit Produk → Pelajari Dulu