# Cara Membuat Multi Stage Build Docker dari Dockerfile Biasa

> Cara membuat multi stage build docker untuk pemula: pecah Dockerfile jadi beberapa stage, salin hanya yang dipakai, jalankan non-root, lalu ukur image.

URL: https://www.matthewswong.com/id/blog/cara-membuat-multi-stage-build-docker/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: DevOps

> **Key takeaway:** Cara membuat multi stage build Docker: beri nama stage lewat FROM node:22-alpine AS builder, build aplikasi di sana, lalu mulai stage baru dengan FROM dan ambil hanya hasil build memakai COPY --from=builder. Hanya stage terakhir yang menjadi image. Ukur sebelum dan sesudahnya dengan docker image ls, jangan menebak.

Dockerfile pertamamu jalan. Aplikasinya hidup di container, lalu kamu menjalankan docker image ls dan melihat kolom SIZE jauh lebih besar dari yang kamu kira. Wajar: di Dockerfile satu stage, image yang dipakai untuk mem-build aplikasi adalah image yang sama yang kamu kirim, jadi compiler, source code, dev dependency, dan cache unduhan ikut terbawa. Pemula biasanya tersangkut di tiga titik: tidak tahu cara memisahkan build dari runtime, salah menulis COPY --from, dan tidak punya cara untuk membuktikan bahwa ukurannya memang turun.

Panduan ini mengubah Dockerfile satu stage menjadi multi-stage dalam tujuh langkah, dengan perintah ukur di setiap tahap. Setiap perintah dan flag saya cocokkan dengan dokumentasi resmi Docker dan npm. Aplikasinya sengaja kecil, sebuah server TypeScript berisi satu file, supaya kamu bisa mencoba semuanya di laptop. Pola yang sama dipakai Dockerfile situs ini: stage deps, builder, dan runner, dengan hanya runner yang dikirim ke server.

Mau paham dulu apa yang sebenarnya dikirim image dan kenapa stage lain tertinggal? Ikuti pelajaran gratis

Multi-Stage Build Docker: Memperkecil Image Next.js Nyata

 di jalur DevOps from zero, lengkap dengan penjelasan interaktif.

## Yang kamu butuhkan

Tiga hal ini cukup. Kalau kamu belum pernah menulis Dockerfile sama sekali, baca dulu

panduan belajar Docker dari nol

, lalu kembali ke sini.

Docker terpasang di komputermu dan perintah docker jalan di terminal. Build di panduan ini memakai BuildKit, yaitu builder bawaan Docker saat ini.

Node.js dan npm terpasang, hanya untuk membuat file package-lock.json satu kali di langkah 1.

Satu folder kosong untuk latihan. Semua nama di bawah (devops-lab, runner, builder) adalah placeholder, ganti sesukamu.

## Langkah 1: Siapkan aplikasi dan Dockerfile satu stage

Kita butuh gambaran sebelum perubahan, jadi mulai dari Dockerfile biasa yang mem-build dan menjalankan aplikasi di satu image. Buat file di bawah ini. File .dockerignore penting: ia membuat node_modules dan dist di komputermu tidak ikut tersalin oleh COPY . . di dalam image.

Simpan Dockerfile satu stage itu sebagai Dockerfile.single. Satu baris FROM berarti satu stage: semua yang dibutuhkan untuk mem-build, termasuk TypeScript dan seluruh node_modules, tetap ada di image akhir.

## Langkah 2: Ukur ukuran image sebelum diubah

Jangan mengubah apa pun sebelum punya angka pembanding. Build image single-stage, lalu baca kolom SIZE. Docker menyebut kolom itu sebagai ukuran disk image. Angkamu akan berbeda dari angka orang lain, karena tergantung base image, arsitektur CPU, dan versi paket, jadi catat milikmu sendiri.

Jalankan juga docker history. Perintah ini menampilkan satu baris per layer beserta instruksi yang membuatnya dan ukurannya. Cari layer dari COPY . . dan RUN npm ci: di situlah source code dan node_modules menumpuk, dan keduanya tidak kamu butuhkan untuk sekadar menjalankan hasil compile.

## Langkah 3: Pecah jadi dua stage, builder dan runner

Sekarang ubah menjadi dua stage. Setiap FROM memulai stage baru di atas base image yang bersih, dan AS memberi nama pada stage itu. Stage bernama lebih aman daripada nomor (0, 1, ...), karena COPY tetap benar kalau urutan stage berubah. Stage runner tidak punya source maupun TypeScript. Ia mengambil satu folder dari builder lewat COPY --from.

Dua hal yang perlu dipahami. Pertama, path setelah --from=builder dibaca dari root filesystem stage builder, bukan dari foldermu, jadi ia harus path absolut di dalam stage itu. Kedua, yang dikirim hanya stage terakhir. Stage builder tetap dibuat saat build, tetapi tidak ikut menjadi image yang kamu jalankan atau push.

## Langkah 4: Pisahkan stage deps supaya cache terpakai

Tambahkan stage ketiga yang hanya menginstall paket. Ia menyalin dua file saja, package.json dan package-lock.json, lalu menjalankan npm ci. Builder kemudian mengambil node_modules-nya lewat COPY --from=deps. Dokumentasi npm menyebut npm ci memang untuk lingkungan otomatis seperti CI dan deployment, dan ia mewajibkan adanya package-lock.json.

Manfaatnya terasa di build berikutnya. Kalau kamu hanya mengubah src/server.ts, layer deps tidak berubah, sehingga npm ci tidak diulang. Ini juga susunan Dockerfile situs ini, dengan satu tambahan: di sana npm ci memakai cache mount untuk folder unduhan npm.

> Kalau aplikasimu butuh paket runtime saat dijalankan (misalnya express), runner tetap perlu node_modules. Jangan menyalin seluruh node_modules dari builder. Buat stage tersendiri yang menjalankan npm ci dengan opsi --omit=dev (opsi omit tercantum di dokumentasi npm ci), lalu salin dari stage itu saja.

## Langkah 5: Salin hanya yang dijalankan, dan jangan jalan sebagai root

Di sinilah pemula paling sering salah, jadi bandingkan tiga baris COPY di bawah. COPY tanpa --from membaca dari foldermu (build context), bukan dari stage lain. Dan menyalin seluruh node_modules dari builder membawa kembali semua dev dependency yang baru saja ingin kamu buang.

Lalu tutup runner dengan user non-root. Panduan best practice Docker menyarankan USER non-root kalau service bisa jalan tanpa hak istimewa, dan menyarankan UID dan GID eksplisit. Perhatikan urutannya: COPY tanpa --chown membuat file dimiliki UID dan GID 0, yaitu root, jadi tambahkan --chown sebelum baris USER berlaku.

Stage runner ini baru berisi satu folder hasil build dan satu user. Kalau nanti aplikasimu perlu menulis ke sebuah folder, siapkan folder itu dan berikan ke user app di COPY atau RUN sebelum USER, karena setelah USER berlaku kamu sudah bukan root.

## Langkah 6: Build ulang, bandingkan, lalu jalankan

Build versi multi-stage dengan tag lain, lalu bandingkan dua tag di docker image ls. Jalankan juga id di dalam container untuk membuktikan user-nya, dan curl untuk membuktikan aplikasi tetap hidup. Hasil yang diharapkan: uid=1001 dari perintah id, dan teks hello dari curl.

Jujurlah pada angkanya. Multi-stage membuang perkakas build, tetapi base image tetap menentukan batas bawah ukuran. Pada aplikasi sekecil ini selisihnya bisa tidak dramatis. Selisih besar baru terasa pada aplikasi nyata dengan banyak dev dependency, seperti Next.js, dan di sana pelajaran terkait di bawah menunjukkan caranya.

> Jangan menyimpulkan multi-stage berhasil hanya karena build-nya hijau. Buktikan dengan tiga pengecekan: docker image ls membandingkan ukuran dua tag, docker run --rm nama-image id menunjukkan user bukan root, dan aplikasi menjawab di port-nya. Satu COPY --from yang salah path bisa menghasilkan image kecil yang tidak berjalan sama sekali.

## Langkah 7: Build satu stage saja dengan --target

Kadang kamu ingin melihat isi sebuah stage tengah, misalnya builder, tanpa membangun sampai akhir. Pakai docker build --target diikuti nama stage: build berhenti di stage itu. Dokumentasi Docker menyebut BuildKit hanya mem-build stage yang dibutuhkan target, sedangkan builder lama memproses semua stage sampai target. Stage FROM scratch adalah image kosong, cocok untuk stage yang hanya ada untuk diekspor, dan exporter local menulis isinya ke folder biasa, bukan membuat image.

Satu catatan dari Dockerfile situs ini. Build tanpa --target menghasilkan stage terakhir di file, jadi stage pembantu seperti cache-export saya letakkan sebelum runner, bukan sesudahnya, supaya runner tetap yang dikirim. Stage pembantu juga tidak selalu gratis: kalau cache build hilang, semua stage di atasnya dijalankan ulang.

## Error umum dan cara memperbaikinya

Enam gejala yang paling sering muncul saat pertama kali menulis multi-stage Dockerfile, urut dari yang paling sering.

| Gejala | Penyebab | Perbaikan |
| --- | --- | --- |
| Build berhenti di baris COPY dengan pesan not found, padahal foldernya ada di komputermu | COPY tanpa --from membaca dari build context, dan folder itu dikecualikan .dockerignore atau memang hanya ada di stage builder | Tambahkan --from=builder dan tulis path absolut di dalam stage itu, misalnya COPY --from=builder /app/dist ./dist |
| Build gagal karena nama stage tidak ditemukan, atau image yang ditarik bukan yang kamu maksud | Nama setelah --from salah ketik atau tidak cocok dengan AS. Nama yang tidak dikenal bisa dianggap sebagai nama image dari registry | Samakan nama persis dengan yang ada setelah AS, dan gunakan nama stage, bukan nomor |
| Image multi-stage hampir tidak lebih kecil dari yang single-stage | Seluruh node_modules disalin dari builder, atau base image memang sudah mengisi sebagian besar ukuran | Salin hanya hasil build, bandingkan docker history kedua tag, dan pertimbangkan base image yang lebih kecil |
| Container langsung mati dengan error module not found saat dijalankan | Runner hanya menerima hasil compile, padahal aplikasi butuh paket runtime di node_modules | Buat stage yang menjalankan npm ci --omit=dev dan salin node_modules dari stage itu ke runner |
| Aplikasi gagal menulis file atau folder, dengan error permission denied | File disalin tanpa --chown sehingga dimiliki root, sementara proses berjalan sebagai user app setelah USER | Tambahkan --chown=app:app pada COPY, atau buat folder yang bisa ditulis sebelum baris USER |
| Setiap perubahan kecil di source membuat npm ci berjalan ulang dari awal | COPY . . diletakkan sebelum npm ci, sehingga layer install ikut tidak valid | Salin dulu package.json dan package-lock.json, jalankan npm ci di stage deps, baru salin source di stage builder |

## Checklist sebelum kamu push image

Setiap stage punya nama lewat AS, dan setiap COPY --from memakai nama itu, bukan nomor.

Runner hanya berisi hasil build dan paket runtime, tanpa source, TypeScript, atau dev dependency.

Ukuran dua tag sudah dibandingkan dengan docker image ls dan angkanya kamu catat sendiri.

docker run --rm nama-image id menunjukkan user non-root dengan UID yang kamu tetapkan.

Aplikasi menjawab di port-nya, dan stage pembantu ditaruh sebelum stage yang dikirim.

## Belajar lebih dalam

Penjelasan interaktif tentang apa yang dibawa tiap stage ada di pelajaran gratis

. Kalau aplikasimu Next.js, lihat bagaimana output standalone menentukan isi runner di

, dan kalau ingin base image yang lebih ramping, baca

.

Aturan yang bisa kamu bawa: build di satu stage, jalankan di stage lain, dan salin antar stage hanya apa yang benar-benar dijalankan. Lalu buktikan dengan angka dari docker image ls milikmu sendiri, bukan dari artikel.

## Sumber dan referensi

- Dokumentasi Docker: multi-stage build
- Dokumentasi Docker: referensi Dockerfile (COPY --from, --chown, USER)
- Dokumentasi Docker: docker image ls
- Dokumentasi Docker: build context dan .dockerignore
- Dokumentasi Docker: exporter local dan tar

## Source links

- https://docs.docker.com/build/building/multi-stage/
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/reference/cli/docker/image/ls/
- https://docs.docker.com/build/concepts/context/
- https://docs.docker.com/build/exporters/local-tar/

## FAQ

### Apa itu multi-stage build Docker dan kenapa dipakai?

Multi-stage build adalah Dockerfile dengan lebih dari satu instruksi FROM, dan setiap FROM memulai stage baru. Kamu mem-build aplikasi di stage awal lalu menyalin hanya hasilnya ke stage terakhir, yang menjadi satu-satunya image. Hasilnya image lebih kecil dan tidak membawa source code atau perkakas build.

### Kenapa multi-stage Dockerfile saya error not found di COPY?

COPY tanpa --from membaca dari build context, bukan dari stage lain. Kalau folder itu dikecualikan .dockerignore atau hanya ada di stage builder, build berhenti dengan pesan not found. Tulis COPY --from=builder dan pakai path absolut di dalam stage itu.

### Apakah multi-stage build pasti membuat image jauh lebih kecil?

Tidak selalu. Multi-stage membuang perkakas build dan source dari image akhir, tetapi base image tetap menentukan ukuran minimum. Penghematannya besar untuk aplikasi dengan banyak dev dependency dan kecil untuk aplikasi mungil. Ukur dengan docker image ls pada kedua tag, jangan percaya angka dari artikel.

### Bisakah saya build satu stage saja dari multi-stage Dockerfile?

Bisa. Jalankan docker build --target diikuti nama stage dan build berhenti di stage itu. BuildKit hanya mem-build stage yang dibutuhkan target. Ini berguna untuk melihat isi stage builder atau untuk stage yang hanya mengekspor file.

### Kenapa stage terakhir sebaiknya memakai USER non-root?

Kalau aplikasi berhasil dibobol, proses yang berjalan sebagai root di dalam container punya kuasa jauh lebih besar daripada user tanpa hak istimewa. Panduan best practice Docker menyarankan USER non-root dengan UID dan GID eksplisit. Ingat bahwa COPY membuat file milik root kecuali kamu menambahkan --chown.

---

- HTML version: https://www.matthewswong.com/id/blog/cara-membuat-multi-stage-build-docker/
- English: https://www.matthewswong.com/en/blog/cara-membuat-multi-stage-build-docker/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
