Docker Multi-stage Build: Giảm kích thước image xuống 20MB từ 1GB

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.

Sơ đồ minh họa multi-stage build: Stage 1 (builder) với SDK, source code, compile → Stage 2 (runtime) chỉ copy binary từ stage 1

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).

Biểu đồ so sánh size image: Single-stage (1.2GB) vs Multi-stage alpine (45MB) vs Multi-stage distroless (12MB) vs Multi-stage scratch (8MB)

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:1000 hoặc USER nonroot:nonroot (distroless)
  • Read-only rootfs: docker run --read-only hoặ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:latest hoặ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

  1. ✅ Dùng multi-stage (builder → runtime)
  2. ✅ Base runtime tối thiểu (distroless/scratch/alpine)
  3. ✅ Static linking (Go/Rust) hoặc minimal deps
  4. ✅ Strip symbols (-s -w)
  5. ✅ Non-root user
  6. ✅ .dockerignore đầy đủ
  7. ✅ BuildKit cache mount
  8. ✅ 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.

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Minh họa Tmux pane layout editor bên trái terminal chạy server bên phải logs dưới cùng

Tmux + sessionizer Quản lý terminal workflow như pro

Tmux: Terminal multiplexer bất kỳ developer nào cũng nên biết Tmux cho phép chia cửa sổ terminal thành nhiều pane, tạo nhiều session, detach/attach bất cứ lúc nào. Khi kết…

Xem thêm

HTMX + Alpine.js: Xây Dựng Web Động Không Cần React Hay Vue

Trong thế giới web hiện đại, không còn nhiều ai chọn React hay Vue cho một trang web đơn giản – vì chúng tốn hàng chục MB bundle, require build…

Xem thêm

Hướng dẫn Nginx Reverse Proxy: cấu hình và tối ưu hiệu năng

Trong hạ tầng web hiện đại, Nginx Reverse Proxy đóng vai trò trung gian đón request từ client rồi chuyển tiếp tới backend, giúp ẩn cấu trúc nội bộ, phân…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất