
Docker multi-stage builds: Cách thu nhỏ image xuống 90%
Một trong những thách thức lớn nhất khi dùng Docker là image size quá lớn. Một image Python đơn giản có thể lên tới 1GB nếu chúng ta không cẩn thận. Image to không chỉ lãng phí đĩa và bandwidth mà còn gây ra những vấn đề bảo mật nghiêm trọng: nhiều lớp package không cần thiết tăng bề mặt tấn công. Multi-stage builds trong Docker là giải pháp quý giá giúp cắt giảm image size xuống tới 10% của image truyền thống.

Vấn đề với Dockerfile truyền thống
Dockerfile đơn giản cho một ứng dụng Python web thường trông như sau:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "app:app"]
Image này còn lại toàn bộ công cụ build: pip, gcc, make, header files,… dù chỉ cần runtime để chạy ứng dụng. Kết quả thường là 400MB-1GB cho ứng dụng Python trung bình.
Nhược điểm: Toxic layers — các lớp chứa công cụ build vẫn còn trong image cuối cùng vì Docker không có cách “xóa bỏ” lớp sau khi build xong. Chỉ có thể apt-get purge sau khi cài đặt, nhưng vẫn để lại cache và metadata.
Giải pháp: Multi-stage builds
Multi-stage builds cho phép sử dụng nhiều FROM statements trong một Dockerfile. Mỗi FROM bắt đầu một stage mới, và chúng ta có thể sao chép các artifact cần thiết từ stage này sang stage khác. Stage cuối cùng chỉ chứa các files cần thiết để chạy ứng dụng — không cần compiler, không cần build tools.

Cú pháp cơ bản
# === STAGE 1: Builder ===
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN pip wheel --no-cache-dir -r requirements.txt -w /wheels
# === STAGE 2: Production ===
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY --from=builder /app .
CMD ["gunicorn", "app:app"]
Trong ví dụ này:
- Stage 1 (builder): Dùng để cài đặt dependencies và build wheel packages
- Stage 2 (production): Chỉ sao chép wheels đã build và mã nguồn, không có pip, gcc, hay build tools nào
- COPY –from=builder: Lệnh Copy đặc biệt để lấy artifact từ stage khác
Ảnh hưởng thực tế đến image size
Ứng dụng Django/Flask Python trung bình:
- Single-stage: ~680 MB (python:3.11-slim + dependencies + build tools)
- Multi-stage: ~180 MB (python:3.11-slim + dependencies runtime only)
- Giảm: ~73% image size
Ứng dụng Node.js/Go/Java có thể giảm hơn vì các ngôn ngữ này thường cần build tool nặng (gcc, maven, gradle) để compile mã nguồn.
Best practices khi áp dụng multi-stage
- Chọn base image phù hợp: Sử dụng
-slimhoặc-alpineđể giảm kích thước khởi đầu. Lưu ý: Alpine dùng musl libc có thể gây vấn đề với một số wheel package Python. - Tối ưu thứ tự lớp: Đặt các lớp thay đổi thường xuyên (COPY source code) ở dưới cùng để tận dụng cache Docker tốt nhất.
- Sử dụng build arguments:
ARGcho các giá trị biến môi trường trong build stage (ví dụ: version, commit hash). - Nhận diện artifact cần copy: Thường là
/wheels(Python),/target(Java),dist/hoặcbuild/(Node.js),binary(Go/Rust). - Stage cuối cùng phải sạch: Chỉ chứa files thực sự cần để chạy ứng dụng (runtime, config, static files).
Ví dụ thực tế: Ứng dụng Python FastAPI
Áp dụng cho một ứng dụng FastAPI với dependencies như uvicorn, pydantic, sqlalchemy.
# Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
# Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Build wheels (tùy chọn, nhưng giúp build nhanh hơn khi thay đổi code)
RUN pip wheel --no-cache-dir -r requirements.txt -w /wheels
# === Production stage ===
FROM python:3.11-slim
WORKDIR /app
# Chỉ cần runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/*
# Copy wheels and install
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
# Copy application code
COPY --from=builder /app .
# Security best practices
USER 1000:1000
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Kết quả: Image 185 MB thay vì 720 MB nếu dùng single-stage — giảm 74%.
Multi-stage build nâng cao
Đối với ứng dụng phức tạp, chúng ta có thể có hơn 2 stage:
- Stage 1: Dependencies: Cài đặt và cache dependencies
- Stage 2: Builder: Compile source code (nếu cần)
- Stage 3: Test: Chạy unit/integration tests (optional)
- Stage 4: Production: Chỉ sao chép artifact đã test và runtime
Đặc biệt hữu ích cho:
- Applications với build steps phức tạp: C/C++, Rust, Java với Maven/Gradle
- CI/CD pipelines: Stage test cho phép fail sớm mà không phải rebuild image
- Security hardening: Copy file config cuối cùng từ secret manager hoặc vault
Kiểm tra và gỡ lỗi multi-stage build
Docker cung cấp nhiều công cụ để kiểm tra image multi-stage:
- BuildKit progress:
DOCKER_BUILDKIT=1 docker buildhiển thị chi tiết từng stage - Image history:
docker image historychỉ ra mỗi lớp và kích thước - Dive tool:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latestphân tích nội dung từng lớp - Scan size:
docker scout cvs(Docker Scout) hoặctrivy imageđể kiểm tra lỗ hổng
Kết luận
Multi-stage build không chỉ là kĩ thuật tối ưu hóa kích thước image — đây là phương pháp thực hành tốt nhất để tạo ra các Docker image sạch, bảo mật và hiệu quả cho môi trường production. Khi kết hợp với các thực hành như using distroless images, scanning vulnerabilities, và signing images, multi-stage build tạo thành nền tảng vững chắc cho chiến lược containerization của bất kỳ công ty nào.
Bắt đầu hôm nay: xem lại Dockerfile của bạn, tìm những công cụ build không cần thiết trong image cuối cùng, và áp dụng multi-stage để tạo ra image nhẹ, nhanh và an toàn hơn. Tham khảo thêm tại Docker Multi-stage Build Documentation và Docker Labs examples.
