Danh sách "mẹo giảm dung lượng image" nào cũng liệt kê bảy tám thứ, thường theo thứ tự ngẫu nhiên và không kèm con số. Tôi dựng một image Python cố tình làm sai mọi thứ, rồi sửa từng lỗi một cách độc lập để biết mỗi thủ thuật đáng giá bao nhiêu.
FROM python:3.12
WORKDIR /app
COPY . /app
RUN apt-get update
RUN apt-get install -y curl vim git
RUN pip install -r requirements.txt
CMD ["python","app.py"]
419,4 MB.
Bảng xếp hạng
Mỗi dòng là bản gốc chỉ sửa đúng một thứ:
| # | Thủ thuật | Còn lại | Tiết kiệm |
|---|---|---|---|
| 1 | Multi-stage + nền alpine | 20,2 MB | 399,2 MB |
| 2 | Multi-stage (chỉ chép gói đã cài) | 44,0 MB | 375,4 MB |
| 3 | Đổi nền sang python:3.12-slim |
116,5 MB | 302,9 MB |
| 4 | --no-install-recommends + xoá apt lists |
403,5 MB | 15,9 MB |
| 5 | Bỏ vim và git, chỉ giữ curl |
404,7 MB | 14,7 MB |
| 6 | pip --no-cache-dir |
417,6 MB | 1,8 MB |
| 7 | Gộp ba lệnh RUN làm một |
419,4 MB | -0,0 MB |
Khoảng cách giữa hạng 3 và hạng 4 là hai mươi lần. Nghĩa là mọi công sức tối ưu dòng RUN cộng lại vẫn không bằng một phần mười của việc đổi một chữ trong dòng FROM.
Thủ thuật được khuyên nhiều nhất không tiết kiệm gì
Hàng cuối bảng đáng dừng lại. "Gộp các lệnh RUN để giảm số layer" là lời khuyên xuất hiện trong gần như mọi hướng dẫn Docker. Đo được: 0,0 MB.
Nó có làm giảm số layer:
ban goc (3 lenh RUN): 12 layer
gop lam 1 lenh RUN : 10 layer
Nhưng ít layer hơn không đồng nghĩa nhỏ hơn. Ba gói curl, vim, git vẫn được cài, vẫn nằm trong image, chỉ khác là nằm trong một layer thay vì ba.
Gộp RUN chỉ có tác dụng khi bạn XOÁ thứ gì đó trong cùng lệnh đó. Phép thử:
# tach: cai o RUN 1, xoa o RUN 2
RUN apt-get update && apt-get install -y --no-install-recommends build-essential
RUN rm -rf /var/lib/apt/lists/* && apt-get purge -y build-essential && apt-get autoremove -y
# gop: cai va xoa trong CUNG mot RUN
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
&& apt-get purge -y build-essential && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
| Dung lượng | |
|---|---|
Cài ở RUN 1, xoá ở RUN 2 |
151,5 MB |
Cài và xoá trong cùng RUN |
41,5 MB |
Chênh 109,9 MB — thủ thuật này nhảy từ hạng bét lên hạng tư ngay khi có phép xoá đi kèm.
Lý do là luật đã đo ở phần 2: layer chỉ cộng thêm, không bao giờ trừ đi. Xoá tệp ở layer sau chỉ ghi thêm một dấu "đã xoá"; dữ liệu vẫn nằm trong layer trước và vẫn tính dung lượng.
Nên phát biểu đúng của lời khuyên là: đừng gộp RUN để giảm layer, hãy gộp để phép xoá nằm cùng layer với phép tạo.
Ba hạng đầu đều là cùng một ý tưởng
Nhìn kỹ thì hạng 1, 2, 3 không phải ba thủ thuật khác nhau — chúng là ba mức của cùng một câu hỏi: bao nhiêu thứ trong image này thật sự cần lúc chạy?
- Đổi nền sang
slimbỏ đi công cụ biên dịch, tài liệu, locale màpython:3.12mang theo: 302,9 MB. - Multi-stage bỏ đi cả môi trường cài đặt, chỉ giữ gói đã cài xong: 375,4 MB.
- Cả hai cho 20,2 MB — nhỏ hơn bản gốc hai mươi lần, và vẫn chạy:
$ docker run --rm ct:7 python -c "import flask, requests; print(flask.__version__, requests.__version__)"
3.0.3 2.32.3
Cấu trúc của bản nhỏ nhất:
FROM python:3.12-slim AS xay
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target /goi -r requirements.txt
FROM python:3.12-alpine
WORKDIR /app
COPY --from=xay /goi /goi
COPY app.py .
ENV PYTHONPATH=/goi
CMD ["python","app.py"]
Với Python thì cách này có điều kiện: nó chạy được vì flask và requests là gói thuần Python. Gói có phần mở rộng C thì phải cẩn thận với chuyện musl so với glibc — phần 11 đã đo được grpcio gãy trên Alpine vì thiếu wheel, và thêm công cụ biên dịch đẩy image từ 17,7 lên 104,7 MB.
Thứ tự nên làm
Dựa trên bảng trên, đây là thứ tự tôi sẽ làm nếu cầm một image béo:
- Xem
FROMđang là gì. Đổipython:3.12→python:3.12-slim,node:22→node:22-slim,openjdk→ JRE thay vì JDK. Một dòng, ba trăm megabyte. - Tách tầng build khỏi tầng chạy. Xem phần 9.
- Xem trong image có gì không cần.
docker run --rm <anh> du -sh /* | sort -rh | head. - Gộp cài và xoá vào cùng một
RUN— nhưng chỉ khi có phép xoá. - Kiểm tra
.dockerignore. Phần 7 đo được 19,1 MB rác lọt vào image chỉ vì thiếu tệp này. - Những thứ còn lại —
--no-cache-dir,--no-install-recommends— vẫn nên làm, nhưng đừng bắt đầu từ đó.
Một cách đo đáng tin hơn docker images
Nhắc lại từ phần 2, vì nó ảnh hưởng trực tiếp tới việc đánh giá công sức tối ưu: docker images báo kích thước đã nén và cộng thừa layer dùng chung. Muốn biết image chiếm bao nhiêu đĩa thật:
docker image ls --tree
Cột DISK USAGE là con số thật. Trong phép đo ở phần 2, một image chứa 50 MB dữ liệu báo cáo là 4,0 MB và chiếm 66,1 MB.
Thử ba mươi giây
Tìm xem image của bạn đang chứa gì:
docker run --rm <anh-cua-ban> sh -c 'du -sh /* 2>/dev/null | sort -rh | head -8'
Và xem layer nào to nhất:
docker history <anh-cua-ban> --format '{{.Size}}\t{{.CreatedBy}}' | sort -rh | head -5
Dòng đầu tiên của lệnh thứ hai là chỗ đáng sửa trước. Trong đa số trường hợp tôi từng gặp, nó là dòng FROM.
Phần sau chuyển sang BuildKit: nó khác builder cũ ở đâu, và đo trên cùng một Dockerfile.