Mục lục
- Mục tiêu bài học
- Vấn đề với single-stage build
- Multi-stage syntax
- Ví dụ cơ bản — Python với build deps
- Ví dụ phức tạp — PyTorch wheel
- Build target — build 1 stage cụ thể
- 3 stage: base + test + prod
- Copy file selective giữa stages
- Distroless image
- BuildKit cache mount
- So sánh kích thước
- Common pitfalls
- Bài tiếp theo
Mục tiêu bài học
Sau khi hoàn thành bài này, bạn sẽ:
- ✅ Giải thích được tại sao single-stage build làm image phình to
- ✅ Viết Dockerfile multi-stage với
AS,COPY --from - ✅ Build image Python và PyTorch wheel không chứa compiler
- ✅ Dùng
--targetđể build stage cụ thể (dev / prod) - ✅ Biết khi nào dùng distroless và tradeoff của nó
Vấn đề với single-stage build
Khi build một ứng dụng Python có thư viện C extension (ví dụ numpy, scipy, một số package ML), Dockerfile single-stage thường trông như này:
FROM python:3.11
RUN apt-get update && apt-get install -y \
build-essential gcc cmake git
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Image này chứa tất cả mọi thứ:
- Build time cần:
gcc,cmake,build-essential, dev headers (libpython-dev) — để compile C extension khipip install. - Runtime chỉ cần: Python interpreter + thư viện đã compile + app code.
Kết quả: image ~600MB–1GB dù ứng dụng chỉ cần ~200MB khi chạy. Compiler, header file, git — tất cả đóng gói vào image production mà không dùng đến.
Multi-stage build giải quyết bằng cách tách hai giai đoạn này thành hai stage riêng biệt, và chỉ copy artifact cần thiết sang stage cuối.
Multi-stage syntax
Ba quy tắc cú pháp chính:
- Mỗi
FROMbắt đầu một stage mới. Stage trước đó kết thúc. - Đặt tên stage với
AS <name>sau tên image:FROM python:3.11-slim AS builder. - Copy artifact từ stage trước:
COPY --from=<stage_name> /src/path /dest/path.
Image output cuối cùng là stage cuối cùng trong Dockerfile (hoặc stage được chỉ định với --target). Các stage trước không xuất hiện trong image final — chỉ được dùng như môi trường trung gian.
# syntax=docker/dockerfile:1.7
FROM python:3.11 AS stage-a
# ... làm gì đó ở đây
FROM python:3.11-slim AS stage-b
COPY --from=stage-a /some/path /dest # lấy file từ stage-a
# ... stage-b là image output
Docker Engine 24+ với BuildKit bật mặc định. Dòng # syntax=docker/dockerfile:1.7 kích hoạt cú pháp mới nhất của Dockerfile frontend (cần BuildKit).
Ví dụ cơ bản — Python với build deps
Pattern phổ biến nhất: dùng pip install --user ở stage builder để cài vào /root/.local, sau đó copy thư mục đó sang stage runtime.
# syntax=docker/dockerfile:1.7
# Stage 1: builder — cài đặt dependency
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: runtime — chỉ chứa thứ cần để chạy
FROM python:3.11-slim AS runtime
WORKDIR /app
# Copy installed packages từ builder (không copy gcc, build-essential)
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Giải thích từng phần:
- Stage
builder: càigccvàbuild-essentialđể compile C extension khi pip install. Package được cài vào/root/.localnhờ--user. - Stage
runtime: dùng cùng base image (python:3.11-slim) nhưng không càigcchaybuild-essential. COPY --from=builder /root/.local /root/.local: chỉ copy thư viện đã compile, không copy compiler.ENV PATH=/root/.local/bin:$PATH: đảm bảo binary của package (nhưuvicorn) trong/root/.local/bintìm thấy được.
Kết quả điển hình với app FastAPI + scikit-learn: single-stage ~650MB, multi-stage ~230MB.
Ví dụ phức tạp — PyTorch wheel
Khi requirements.txt có nhiều package cần compile (như tokenizers, sentencepiece), dùng pip wheel để build tất cả thành .whl trước, sau đó install trong runtime không cần compiler.
# syntax=docker/dockerfile:1.7
# Stage 1: builder — build wheel cho tất cả package
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY requirements.txt .
# Build wheel vào /wheels, không install
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# Stage 2: runtime — install từ wheel, không cần compiler
FROM python:3.11-slim AS runtime
# Chỉ cài runtime system lib (libgomp cho OpenMP, libstdc++ cho C++ ABI)
RUN apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
libstdc++6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels
# Non-root user
RUN useradd -m -u 1000 appuser
USER appuser
COPY --chown=appuser:appuser . .
EXPOSE 8000
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Điểm khác biệt so với ví dụ trước:
pip wheel --wheel-dir /wheels: build.whlnhưng không install. Wheel file là binary đã compile, có thể install trên runtime tương thích mà không cần compiler.- Stage runtime vẫn cần một số system lib:
libgomp1(OpenMP, dùng bởi numpy/sklearn),libstdc++6(C++ runtime). Đây là runtime dependency, không phải build tool. rm -rf /wheelssau install để không để lại file .whl trong layer cuối.- Non-root user
appuser: best practice bảo mật — container không chạy với root.
Lưu ý quan trọng về PyTorch: PyTorch chính thức phân phối pre-built wheel trên PyPI và index.torch.org — không cần compile từ source. Tuy nhiên các package nhỏ đi kèm như tokenizers (Rust), sentencepiece (C++) vẫn cần compiler khi không có pre-built wheel cho platform của bạn.
Build target — build 1 stage cụ thể
Flag --target chỉ build đến một stage cụ thể, bỏ qua các stage sau đó:
# Build đến stage builder (image dev có đầy đủ tool)
docker build --target builder -t my-app:dev .
# Build toàn bộ, output là stage cuối (runtime/prod)
docker build -t my-app:prod .
Use case thực tế:
- Local dev:
--target builderđể có image chứa đủ tool debug, shell, build toolchain. - CI/CD: build
--target testđể chạy test, sau đó build production image riêng. - Troubleshoot: build đến stage gặp vấn đề để inspect filesystem (
docker run -it my-app:dev bash).
Khi không có --target, Docker build toàn bộ Dockerfile và output là stage cuối cùng.
3 stage: base + test + prod
Pattern 3 stage cho phép tích hợp test vào Dockerfile, dùng chung 1 file cho CI và production.
# syntax=docker/dockerfile:1.7
# Stage 1: base — dependency chung
FROM python:3.11-slim AS base
WORKDIR /app
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Stage 2: test — thêm test dependency, chạy test
FROM base AS test
RUN pip install --no-cache-dir pytest pytest-cov httpx
COPY tests ./tests
RUN pytest tests/ -v --tb=short
# Stage 3: prod — từ base, không có test dependency
FROM base AS prod
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Workflow CI/CD:
# CI: build đến stage test — sẽ fail nếu test fail
docker build --target test -t my-app:test .
# Deploy: build production image
docker build --target prod -t my-app:prod .
docker push registry.example.com/my-app:prod
Vì stage test extends base (kế thừa layer), Docker cache tái sử dụng được. Khi code không thay đổi, layer pip install được cache, chỉ re-run test.
Lưu ý: RUN pytest tests/ trong Dockerfile — nếu test fail, docker build trả về exit code khác 0, CI pipeline sẽ dừng. Đây là cách tích hợp test vào build pipeline đơn giản nhất.
Copy file selective giữa stages
COPY --from không bắt buộc copy toàn bộ filesystem của stage trước. Có thể chỉ copy file hoặc folder cụ thể.
Ví dụ thực tế với AI: convert model sang ONNX ở stage builder, runtime chỉ cần file .onnx kết quả.
# syntax=docker/dockerfile:1.7
# Stage 1: converter — cần PyTorch đầy đủ để export ONNX
FROM python:3.11 AS converter
RUN pip install --no-cache-dir torch onnx
WORKDIR /build
COPY convert_model.py model.pth ./
# Export sang ONNX
RUN python convert_model.py model.pth model.onnx
# Stage 2: runtime — chỉ cần onnxruntime, không cần torch
FROM python:3.11-slim AS runtime
RUN pip install --no-cache-dir onnxruntime
WORKDIR /app
# Chỉ copy file ONNX, không copy toàn bộ stage converter
COPY --from=converter /build/model.onnx /app/model.onnx
COPY app/ ./app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Trong ví dụ này:
- Stage
converter: cài PyTorch (~2GB), chạy script export ONNX. - Stage
runtime: chỉ cầnonnxruntime(~100MB). Copy mỗi filemodel.onnxtừconverter. - Image final không chứa PyTorch, không chứa model checkpoint
.pthgốc.
COPY --from cũng có thể lấy từ image bên ngoài (không phải stage trong cùng Dockerfile):
# Copy binary từ image official thay vì cài qua apt
COPY --from=bitnami/python:3.11 /usr/local/bin/python /usr/local/bin/python
Dùng ít hơn nhưng hữu ích khi cần binary từ image khác mà không muốn cài qua package manager.
Distroless image
Google Distroless (github.com/GoogleContainerTools/distroless) là tập hợp base image không có shell, package manager, hay bất kỳ tiện ích nào ngoài runtime cần thiết.
Image Python distroless: gcr.io/distroless/python3-debian12 — chỉ chứa Python interpreter + glibc, khoảng 40MB so với python:3.11-slim ~130MB.
Multi-stage với distroless:
# syntax=docker/dockerfile:1.7
# Stage 1: builder — cần full Python để install deps
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: runtime — distroless, không có bash
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
# Copy site-packages từ builder
COPY --from=builder /install/lib/python3.11/site-packages \
/usr/local/lib/python3.11/site-packages
COPY --from=builder /install/bin /usr/local/bin
COPY app/ ./app/
# Distroless không có shell, CMD phải là executable trực tiếp
CMD ["app/main.py"]
Tradeoffs của distroless:
- Lợi: Image nhỏ hơn (~40% so với slim), attack surface nhỏ (không có shell → attacker không thể mở interactive session ngay cả khi có RCE).
- Hạn chế: Không có
bash,sh→docker exec -it container bashsẽ fail. Debug production container trở nên khó hơn đáng kể. - Workaround debug: Dùng variant debug:
gcr.io/distroless/python3-debian12:debug— có thêm busybox shell nhưng không dùng cho production.
Distroless phù hợp với team đã có observability (logging, metrics) tốt và không cần docker exec trực tiếp để debug. Với team nhỏ hoặc đang prototype, python:3.11-slim + multi-stage đã đủ.
BuildKit cache mount
BuildKit (Docker Engine 23.0+ bật mặc định) hỗ trợ --mount=type=cache để cache thư mục pip giữa các lần build mà không đưa cache vào image final.
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
# Cache /root/.cache/pip giữa các build, không vào image final
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --user -r requirements.txt
FROM python:3.11-slim AS runtime
WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Không có --mount=type=cache: mỗi lần build mà requirements.txt thay đổi, pip download lại toàn bộ.
Với cache mount: pip sử dụng lại package đã download từ build trước, chỉ download package mới hoặc phiên bản mới. Cache được lưu trên host, không trong image.
Cần phân biệt: cache mount và --no-cache-dir. Khi dùng cache mount, bỏ flag --no-cache-dir — hai cơ chế không xung đột nhưng dùng cả hai thì cache mount vô nghĩa.
Cache mount cũng hoạt động với apt:
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc
So sánh kích thước
Số liệu tham khảo với một FastAPI app + scikit-learn + numpy + pillow:
| Cách build | Base image | Kích thước xấp xỉ |
|---|---|---|
| Single-stage | python:3.11 |
~600–700MB |
| Single-stage | python:3.11-slim |
~400–450MB |
| Multi-stage | runtime: python:3.11-slim |
~200–250MB |
| Multi-stage | runtime: distroless python3 | ~120–160MB |
Số liệu thực tế thay đổi theo danh sách dependency. App có PyTorch (~2GB) sẽ lớn hơn nhiều — dùng PyTorch chỉ ở stage convert rồi export ONNX (bài 8 ở trên) là cách giảm kích thước đáng kể nhất cho inference app.
Để kiểm tra kích thước image:
docker images my-app
# REPOSITORY TAG IMAGE ID CREATED SIZE
# my-app prod a1b2c3d4e5f6 2 minutes ago 228MB
# my-app dev f6e5d4c3b2a1 3 minutes ago 651MB
Để xem từng layer đóng góp bao nhiêu:
docker history my-app:prod
Common pitfalls
1. Quên --from trong COPY
# SAI: copy từ build context, không phải từ stage builder
COPY /root/.local /root/.local
# ĐÚNG: chỉ định --from
COPY --from=builder /root/.local /root/.local
2. Copy site-packages nhưng thiếu system lib runtime
Một số package cần system library khi chạy (không phải khi build). Ví dụ numpy cần libgomp1 (OpenMP), package có Fortran cần libgfortran5. Nếu runtime image không có lib đó, import sẽ fail với lỗi như:
ImportError: libgomp.so.1: cannot open shared object file: No such file or directory
Cách kiểm tra lib nào package cần:
# Chạy trong container runtime sau khi gặp lỗi:
ldd /root/.local/lib/python3.11/site-packages/numpy/core/_multiarray_umath.so
3. Pip user install thiếu PATH
Khi dùng pip install --user, binary được cài vào /root/.local/bin. Nếu không set ENV PATH=/root/.local/bin:$PATH, lệnh uvicorn, gunicorn sẽ không tìm thấy.
4. Distroless: docker exec bash fail
# Sẽ fail với distroless production image
docker exec -it container bash
# OCI runtime exec failed: exec failed: unable to start container process:
# exec: "bash": executable file not found
# Debug: dùng debug variant
docker run -it gcr.io/distroless/python3-debian12:debug sh
5. Stage name trùng hoặc không nhất quán
Nếu đặt tên stage là builder ở Dockerfile A và build ở Dockerfile B, script CI dùng --target builder sẽ fail ở Dockerfile B. Thống nhất naming convention trong dự án.
6. Build target sai dẫn đến image thiếu app code
FROM python:3.11-slim AS deps
RUN pip install -r requirements.txt
FROM deps AS app
COPY . . # App code chỉ có ở stage "app"
CMD ["uvicorn", ...]
# Sai: image không có app code
docker build --target deps -t my-app:prod .
# Đúng: build đến stage có COPY app code
docker build --target app -t my-app:prod .
# hoặc không dùng --target (default = stage cuối)
docker build -t my-app:prod .
