
Docker Multi-stage Build: Giảm kích thước image xuống 20MB từ 1GB
Docker multi-stage build là kỹ thuật chia Dockerfile thành nhiều giai đoạn (stage), mỗi stage dùng base image khác nhau và chỉ copy artifact cần thiết sang stage cuối. Kết quả: image production siêu nhẹ — chỉ chứa binary/app runtime, không có build tools, source code, test suite hay dependency dev. Một Go/Rust app thường giảm từ 1GB+ xuống 20-50MB (scratch/distroless) hoặc 200-300MB (alpine/slim).
Kỹ thuật này được Docker hỗ trợ từ phiên bản 17.05 (2017) và hiện là best practice bắt buộc cho mọi Dockerfile production. Xem tài liệu chính thức: Docker Docs – Multi-stage builds.

Vấn đề: Dockerfile single-stage truyền thống
Dockerfile truyền thống dùng một base image lớn (golang:1.21, node:20, python:3.11) cho cả build và runtime:
# Dockerfile single-stage - KHÔNG KHUYẾN NGHỊ
FROM golang:1.21
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp .
CMD ["./myapp"]
Kết quả: Image ~1.2GB (chứa Go SDK, compiler, source, cache, module cache). Triển khai chậm, tốn bandwidth, surface attack lớn, vi phạm principle of least privilege.
Giải pháp: Multi-stage build
# syntax=docker/dockerfile:1
# Stage 1: Builder - có đầy đủ SDK, tools
FROM golang:1.21 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/myapp .
# Stage 2: Runtime - chỉ có binary, base siêu nhẹ
FROM gcr.io/distroless/static-debian12 AS runtime
COPY --from=builder /bin/myapp /bin/myapp
USER nonroot:nonroot
ENTRYPOINT ["/bin/myapp"]
Kết quả: Image ~12MB (distroless static) hoặc ~20MB (alpine). Chỉ chứa binary static-linked, không shell, không package manager, không libc (nếu static).

Các base image runtime phổ biến
| Base Image | Size | Đặc điểm | Phù hợp |
|---|---|---|---|
scratch |
0B | Rỗng hoàn toàn, chỉ binary static | Go/Rust static binary, C/C++ static |
gcr.io/distroless/static-debian12 |
~2-3MB | Non-root, certs, timezone, no shell | Production Go/Rust, bảo mật cao |
gcr.io/distroless/base-debian12 |
~10MB | Có libc, ca-certs, no shell | App dynamic linking (Java, Python) |
alpine:3.20 |
~5MB | Musl libc, apk, shell có sẵn | General purpose, debug dễ dàng |
debian:12-slim |
~80MB | glibc đầy đủ, apt | App phức tạp cần glibc full |
python:3.12-slim |
~120MB | Python runtime + pip | Python app production |
node:20-alpine |
~60MB | Node.js + npm | Node.js app production |
Lưu ý: Alpine dùng musl libc, có thể gây bug tương thích với một số binary glibc (memory allocator, DNS). Test kỹ trước khi production.
Tối ưu thêm cho từng ngôn ngữ
Go: Static linking + strip symbols
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/app .
# -s: strip symbol table, -w: strip DWARF debug info
# CGO_ENABLED=0: ép static linking, không phụ thuộc glibc
Rust: Cargo chef + strip
# Stage 1: Cache dependencies với cargo-chef
FROM lukemathwalker/cargo-chef:latest-rust-1.78 AS planner
WORKDIR /app
COPY . .
RUN cargo chef prepare --recipe.json
FROM lukemathwalker/cargo-chef:latest-rust-1.78 AS builder
WORKDIR /app
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe.json
COPY . .
RUN cargo build --release --bin myapp
# Stage 2: Runtime
FROM debian:12-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
Node.js: Standalone output + alpine
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime - Next.js standalone output
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
CMD ["node", "server.js"]
Python: UV / pip install –user + slim
# Stage 1: Build wheels
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock* ./
RUN pip install --no-cache-dir uv && uv pip install --system --no-cache .
# Stage 2: Runtime
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
USER 1000:1000
CMD ["python", "main.py"]
Các kỹ thuật nâng cao
Named stages & –target
Đặt tên stage bằng AS và dùng COPY --from=. Build đến stage cụ thể:
docker build --target builder -t myapp:dev .
docker build --target runtime -t myapp:prod .
Hữu ích cho: debug stage (có tools), test stage (có test runner), production stage (minimal).
External image as stage
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=my-private-registry.com/base:v1.2 /usr/lib/libcustom.so /usr/lib/
Copy artifact từ image riêng biệt (registry, local) mà không cần rebuild.
BuildKit cache mount
# syntax=docker/dockerfile:1.4
FROM golang:1.21 AS builder
RUN --mount=type=cache,target=/go/pkg/mod
--mount=type=cache,target=/root/.cache/go-build
go build -o /bin/app .
Cache module download và build cache giữa các lần build, giảm thời gian CI/CD đáng kể.
.dockerignore – Bắt buộc
# .dockerignore
.git
.gitignore
README.md
Dockerfile*
docker-compose*.yml
*.md
tests/
*.test
coverage/
.idea/
.vscode/
__pycache__/
*.pyc
node_modules/
dist/
build/
.tmp/
*.log
Loại trừ file không cần thiết khỏi build context — giảm context size, tăng tốc build, tránh leak secret.
Security hardening
- Non-root user:
USER 1000:1000hoặcUSER nonroot:nonroot(distroless) - Read-only rootfs:
docker run --read-onlyhoặc config trong Kubernetes - Drop capabilities:
--cap-drop=ALL - No shell: Distroless/scratch không có sh/bash → giảm attack surface
- Scan image:
docker scout cves myapp:latesthoặc Trivy, Grype
Debug multi-stage image
Vì production image không có shell, debug bằng:
# Build debug stage
docker build --target builder -t myapp:debug .
docker run -it myapp:debug sh
# Hoặc copy binary ra host
docker create --name temp myapp:prod
docker cp temp:/bin/myapp ./myapp
docker rm temp
Tóm tắt checklist production
- ✅ Dùng multi-stage (builder → runtime)
- ✅ Base runtime tối thiểu (distroless/scratch/alpine)
- ✅ Static linking (Go/Rust) hoặc minimal deps
- ✅ Strip symbols (-s -w)
- ✅ Non-root user
- ✅ .dockerignore đầy đủ
- ✅ BuildKit cache mount
- ✅ Scan CVE trước deploy
Multi-stage build không chỉ giảm size — nó tách biệt build-time concerns khỏi runtime concerns, làm Dockerfile dễ maintain, CI/CD nhanh hơn, và production an toàn hơn. Áp dụng ngay hôm nay cho mọi project.
