Pelajari bagaimana Docker multi-stage build secara dramatis mengurangi ukuran container image, meningkatkan keamanan, dan mempercepat deployment. Contoh dunia nyata dengan aplikasi Node.js, Go, dan Python.

Anda telah membangun aplikasi, mengontainerkannya dengan Docker, dan push ke produksi. Semuanya berfungsi baik sampai Anda menyadari image Node.js 1.2GB Anda memakan waktu selamanya untuk pull di seluruh cluster, pipeline CI/CD Anda merayap, dan tagihan container registry Anda terus meningkat.
Di sinilah sebagian besar tim menyadari Dockerfile mereka memerlukan optimasi serius. Penyebabnya? Termasuk build tool, source code, dan dependency yang tidak seharusnya ada dalam image produksi.
Docker multi-stage build menyelesaikan ini dengan memisahkan build-time dependency dari runtime requirement. Hasilnya? Image yang 70-90% lebih kecil, lebih aman, dan lebih cepat di-deploy. Ini bukan hanya tentang menghemat disk space—ini tentang mengurangi attack surface, mempercepat deployment, dan memotong biaya infrastruktur.
Multi-stage build memungkinkan Anda menggunakan multiple pernyataan FROM dalam satu Dockerfile. Setiap instruksi FROM memulai build stage baru, dan Anda dapat secara selektif menyalin artifact dari satu stage ke stage lain, meninggalkan di belakang semuanya yang tidak Anda butuhkan.
Anggap saja seperti memasak: Anda menggunakan mangkuk pencampur, cangkir ukur, dan whisk untuk menyiapkan kue, tetapi Anda tidak menyajikan alat-alat itu di piring. Multi-stage build memungkinkan Anda menggunakan semua alat yang Anda butuhkan selama proses build, kemudian kemasan hanya produk akhir.
Sebelum multi-stage build ada (pre-Docker 17.05), developer harus menggunakan builder pattern dengan multiple Dockerfile atau script shell kompleks untuk mencapai hasil serupa. Itu berantakan dan rawan kesalahan.
Setiap kali Anda deploy, node Kubernetes pull image container Anda. Image 1.2GB di atas jaringan memakan waktu jauh lebih lama daripada image 120MB. Kalikan ini di seluruh puluhan node selama rolling update, dan Anda sedang melihat menit delay yang tidak perlu.
Dalam skenario incident response, menit-menit itu penting. Ketika Anda perlu scale up dengan cepat atau rollback deployment buruk, waktu pull image secara langsung mempengaruhi mean time to recovery (MTTR) Anda.
Setiap paket, library, dan binary dalam image Anda adalah potensi vulnerability. Build tool seperti gcc, make, npm, pip, dan dependency mereka menambahkan ratusan paket yang tidak Anda butuhkan saat runtime. Masing-masing adalah potensi CVE yang menunggu terjadi.
Security scanner seperti Trivy atau Snyk akan flag vulnerability ini, menciptakan noise dan risiko nyata. Image lebih kecil berarti lebih sedikit paket untuk patch dan audit.
Container registry mengenakan biaya untuk storage dan bandwidth. Cloud provider mengenakan biaya untuk data transfer. Ketika Anda pull image multi-gigabyte di seluruh region atau availability zone, biaya itu bertambah dengan cepat.
Tim yang menjalankan 100 deployment per hari dengan image 1GB mentransfer 100GB setiap hari. Kurangi menjadi image 100MB, dan Anda di 10GB—pengurangan 90% dalam biaya bandwidth.
Mari kita lihat aplikasi Node.js nyata untuk memahami perbedaannya.
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]Dockerfile ini bekerja, tetapi memiliki masalah kritis:
devDependencies (TypeScript, webpack, testing library)# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production=false
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]Pendekatan ini:
Berikut adalah multi-stage build production-ready untuk TypeScript Node.js API:
# Dependencies stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Production stage
FROM node:20-alpine
RUN apk add --no-cache dumb-init
ENV NODE_ENV=production
USER node
WORKDIR /app
COPY --chown=node:node --from=deps /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/dist ./dist
COPY --chown=node:node package.json ./
EXPOSE 3000
CMD ["dumb-init", "node", "dist/index.js"]Tip
Perhatikan stage deps terpisah. Ini cache production dependency secara independen, jadi rebuild lebih cepat ketika hanya source code berubah.
Aplikasi Go mendapat manfaat lebih besar dari multi-stage build karena compiled Go binary adalah self-contained:
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Production stage
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/main /main
EXPOSE 8080
CMD ["/main"]Ini menghasilkan image sekecil 10-20MB. Direktif FROM scratch membuat image base kosong—benar-benar tidak ada kecuali binary Anda dan sertifikat SSL.
Important
Menggunakan scratch berarti tidak ada shell, tidak ada package manager, tidak ada debugging tool. Ini sangat baik untuk keamanan tetapi dapat membuat troubleshooting lebih sulit. Pertimbangkan menggunakan alpine jika Anda memerlukan utility dasar.
Aplikasi Python memerlukan lebih banyak perhatian karena runtime dependency:
# Build stage
FROM python:3.11 AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]Virtual environment Python membuat mudah untuk menyalin hanya installed package tanpa build tool.
Docker cache layer, tetapi hanya jika tidak ada yang berubah di atas mereka. Struktur Dockerfile Anda untuk memaksimalkan cache hit:
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files terlebih dahulu
COPY package.json package-lock.json ./
RUN npm ci
# Copy source code terakhir
COPY . .
RUN npm run buildDengan cara ini, mengubah source code tidak menginvalidasi dependency cache. Build Anda tetap cepat bahkan saat Anda iterate.
Pass build-time variable tanpa hardcoding:
FROM node:20-alpine AS builder
ARG BUILD_VERSION=dev
ARG API_URL
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN BUILD_VERSION=${BUILD_VERSION} API_URL=${API_URL} npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]Build dengan argument:
docker build \
--build-arg BUILD_VERSION=1.2.3 \
--build-arg API_URL=https://api.example.com \
-t myapp:1.2.3 .Build image untuk arsitektur AMD64 dan ARM64:
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN GOARCH=${TARGETARCH} go build -o main .
FROM alpine:latest
COPY --from=builder /app/main /main
CMD ["/main"]Build untuk multiple platform:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
--push .Ini crucial untuk tim yang menjalankan mixed infrastructure atau deploy ke ARM-based instance seperti AWS Graviton.
Jangan copy semuanya secara membabi buta:
COPY . .COPY package*.json ./
COPY src ./src
COPY tsconfig.json ./
COPY public ./publicLebih baik lagi, gunakan .dockerignore:
node_modules
npm-debug.log
.git
.env
.env.local
dist
coverage
*.md
.vscode
.ideaIni mengalahkan tujuan multi-stage build:
RUN npm installRUN npm ci --only=productionPilihan base image penting secara signifikan:
node:20 → 1.1GBnode:20-slim → 240MBnode:20-alpine → 135MBAlpine menggunakan musl libc daripada glibc, yang dapat menyebabkan masalah kompatibilitas dengan beberapa native module. Test secara menyeluruh, tetapi untuk sebagian besar aplikasi, Alpine bekerja sempurna.
Package manager cache download, membengkak image Anda:
# Node.js
RUN npm ci --only=production && npm cache clean --force
# Python
RUN pip install --no-cache-dir -r requirements.txt
# Alpine apk
RUN apk add --no-cache dumb-initSelalu drop privilege dalam produksi:
FROM node:20-alpine
USER node
WORKDIR /app
COPY --chown=node:node package*.json ./
RUN npm ci --only=production
COPY --chown=node:node . .
CMD ["node", "index.js"]Jangan pernah gunakan latest dalam produksi:
FROM node:latestFROM node:20.11.0-alpine3.19Tag spesifik memastikan reproducible build dan mencegah breakage yang tidak terduga ketika base image update.
Tambahkan health check langsung dalam Dockerfile Anda:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
CMD ["node", "index.js"]Kurangi build context size dan cegah kebocoran secret:
# Dependencies
node_modules
vendor
# Build artifacts
dist
build
*.log
# Development
.git
.github
.vscode
.idea
*.md
.env*
!.env.example
# Testing
coverage
test
*.test.js
*.spec.ts
# CI/CD
.gitlab-ci.yml
.github
JenkinsfileIntegrasikan security scanning ke dalam CI/CD Anda:
trivy image --severity HIGH,CRITICAL myapp:latestsnyk container test myapp:latestAktifkan Docker BuildKit untuk performa dan fitur lebih baik:
export DOCKER_BUILDKIT=1
docker build -t myapp:latest .Gunakan BuildKit cache mount:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run buildBuildKit cache mount persist antar build, secara dramatis mempercepat dependency installation.
Mari kita kuantifikasi improvement dengan angka nyata dari production Node.js API:
Ukuran image: 1.24 GB
Layer: 12
Vulnerability: 247 (23 critical)
Pull time (cold): 3m 42s
Build time: 4m 18sUkuran image: 187 MB (pengurangan 85%)
Layer: 8
Vulnerability: 12 (0 critical)
Pull time (cold): 28s (87% lebih cepat)
Build time: 2m 51s (34% lebih cepat)Improvement keamanan saja membenarkan usaha. Pergi dari 23 critical vulnerability ke nol menghilangkan seluruh kelas risiko.
Multi-stage build bukan selalu jawabannya:
Untuk development lokal, single-stage build dengan hot-reloading lebih praktis. Anda menginginkan fast iteration, bukan minimal size:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]Jika Anda mengontainerkan script bash sederhana atau utility yang berjalan sekali dan keluar, kompleksitasnya tidak layak.
Jika Anda mulai dari scratch atau base minimal, tidak ada yang perlu dioptimalkan.
Beberapa aplikasi memiliki tangled dependency yang membuat multi-stage build tidak praktis. Kadang usaha untuk untangle mereka melebihi manfaatnya.
Ketika build gagal atau image tidak bekerja seperti yang diharapkan:
Test individual stage:
docker build --target builder -t myapp:builder .docker run -it myapp:builder shTool dive menunjukkan persis apa yang ada di setiap layer:
wget https://github.com/wagoodman/dive/releases/download/v0.11.0/dive_0.11.0_linux_amd64.deb
sudo dpkg -i dive_0.11.0_linux_amd64.debdive myapp:latestIni mengungkap wasted space dan membantu mengidentifikasi apa yang membengkak image Anda.
Lihat apa yang di-cache:
docker buildx dudocker builder pruneMulti-stage build essential untuk production Docker image. Mereka mengurangi size sebesar 70-90%, menghilangkan unnecessary vulnerability, dan mempercepat deployment—semuanya dengan usaha minimal.
Pola itu sederhana: gunakan full-featured image untuk build, kemudian copy hanya apa yang Anda butuhkan ke minimal runtime image. Separasi build-time dan runtime concern ini fundamental untuk container best practice.
Mulai dengan contoh dalam panduan ini, adaptasikan ke stack Anda, dan ukur hasilnya. Anda akan melihat improvement segera dalam deployment speed, security posture, dan infrastructure cost.
Lain kali Anda menulis Dockerfile, tanyakan pada diri sendiri: apakah ini perlu dalam produksi? Jika jawabannya tidak, tinggalkan di build stage.