Bài 1

Lệnh nào làm vỡ cache khi build

Docker dùng lại kết quả của một bước khi đầu vào của bước đó không đổi. Câu đó nghe đơn giản, nhưng "đầu vào" gồm những gì thì phải đo mới biết: nội dung file hay cả thời gian sửa, cả thư mục hay chỉ file được chép, và một dòng ghi chú có tính không. Bài này đo mười bốn trường hợp, mỗi lần chỉ đổi một thứ.

Chín kịch bản trên một Dockerfile

FROM alpine:3.22
RUN echo "buoc-1" > /b1
COPY deps.txt /deps.txt
RUN echo "buoc-2" > /b2
COPY app.txt /app.txt
RUN echo "buoc-3" > /b3

Mỗi hàng dưới đây là một lần build đầy đủ sau khi đã hâm cache. Cột kết quả đọc theo thứ tự sáu bước ở trên: C là CACHED, R là chạy lại.

Thay đổiFROM · b1 · deps · b2 · app · b3Số bước chạy lại
không đổi gìCCCCCC0
đổi nội dung app.txtCCCCRR2
đổi nội dung deps.txtCCRRRR4
touch app.txt, nội dung giữ nguyênCCCCCC0
thêm một file không lệnh nào COPYCCCCCC0
sửa dòng RUN cuốiCCCCCR1
sửa dòng RUN đầuCRRRRR5
thêm một dòng ghi chú vào DockerfileCCCCCC0
thêm .dockerignoreCCCCCC0
Ba điều bảng này nói ra Khoá cache của COPY là nội dung file, không phải thời gian sửa — hàng touch vẫn CACHED. COPY app.txt chỉ nhìn đúng file đó, nên thêm file khác vào thư mục không ảnh hưởng gì. Và dòng ghi chú không nằm trong khoá cache.
Cache vỡ thì vỡ xuôi xuống tận cuối So hai hàng deps.txt và app.txt: cùng một thao tác "đổi nội dung một file", nhưng file được chép sớm hơn làm bốn bước chạy lại còn file chép muộn chỉ làm hai. Bước RUN echo "buoc-2" không đọc deps.txt, không liên quan gì tới nó, vẫn phải chạy lại — vì cache của Docker là một chuỗi, mất một mắt thì mất hết phần sau.

Một chú thích về đọc log: dòng [1/6] FROM … luôn hiện ra kể cả khi không có gì đổi. Đó là BuildKit phân giải lại tham chiếu ảnh nền, không phải build lại layer. Bảng trên đã tính FROM là CACHED ở mọi hàng trừ hàng sửa chính dòng đó.

COPY một file khác COPY cả thư mục

Dockerfile thứ hai, ngắn hơn, để tách riêng hành vi của COPY .:

FROM alpine:3.22
COPY . /src
RUN echo "buoc-1" > /b1
Thay đổiFROM · COPY . · RUNNghĩa là
không đổi gìCCC—
thêm một file lạ vào thư mụcCRRCOPY . nhìn cả thư mục, nên tập file chép đã đổi
chỉ touch file lạ đóCCCvẫn là nội dung, không phải thời gian
thêm .dockerignore bỏ file lạCRRtập file chép đổi lần nữa, nên vỡ một lần
đổi file lạ sau khi đã bị bỏCCCfile không còn nằm trong tập được chép

Hai hàng cuối là lý do nên thêm .dockerignore sớm: bản thân việc thêm nó làm vỡ cache một lần, nhưng sau đó mọi thay đổi trong node_modules, .git, dist đều không còn đụng tới build nữa.

BuildKit chỉ gửi file nó cần

Một niềm tin phổ biến từ thời builder cũ: không có .dockerignore thì Docker nén cả thư mục rồi gửi sang daemon. Với BuildKit thì không còn đúng. Đo trên một thư mục 15 MB (node_modules/blob.bin 15 MB cộng app.js vài byte):

Dockerfile.dockerignoretransferring contextKích thước ảnh
COPY app.js /app.jskhông37 B—
COPY app.js /app.jsbỏ node_modules27 B—
COPY . /srckhông15,73 MB19 859 207 B
COPY . /srcbỏ node_modules107 B4 125 651 B
Hệ quả thực tế Nếu Dockerfile của bạn chỉ COPY đúng những file cần, thì .dockerignore gần như không thay đổi lượng dữ liệu gửi đi — 37 byte so với 27 byte. Nó chỉ trở nên quan trọng khi có COPY ., và ở đó nó quan trọng thật: 15,73 MB xuống 107 byte, và ảnh từ 19,9 MB xuống 4,1 MB.

Phòng thí nghiệm

Gõ Dockerfile của bạn, khai báo thứ vừa đổi, và xem từng bước. Thuật toán chạy trên trình duyệt đã đối chiếu với 14 kịch bản build thật trên Docker 29.7.2 — khớp toàn bộ.

Dockerfile

Bạn vừa đổi gì

Các kịch bản đã đo:

Từng bước khi build lại

    Hệ quả: thứ tự dòng trong Dockerfile

    Bảng đầu tiên nói rằng chi phí của một thay đổi bằng số bước đứng sau bước bị vỡ. Vậy thì thứ tự dòng là một quyết định về hiệu năng build, không phải về thẩm mỹ. Nguyên tắc rút ra từ số đo: đặt thứ ít đổi lên trên, thứ hay đổi xuống dưới.

    # thu tu hay gap, va ton kem
    COPY . /app
    RUN npm ci
    RUN npm run build
    
    # thu tu re hon: danh sach phu thuoc doi it hon ma nguon
    COPY package.json package-lock.json /app/
    RUN npm ci
    COPY . /app
    RUN npm run build

    Ở bản trên, sửa một dòng mã bất kỳ làm npm ci chạy lại. Ở bản dưới, npm ci chỉ chạy lại khi package-lock.json đổi — đúng cơ chế đã đo ở hàng deps.txt so với app.txt.

    Tự kiểm tra

    Dockerfile có 10 bước, bạn sửa dòng thứ 3. Bao nhiêu bước chạy lại? Tám bước: dòng thứ 3 và cả bảy dòng sau nó. Cache của Docker là một chuỗi, nên chi phí của một thay đổi bằng số bước đứng sau nó, không phụ thuộc bước đó có liên quan hay không.
    Bạn chạy git checkout làm mtime của mọi file đổi, nhưng nội dung phần lớn file giữ nguyên. Cache còn không? Còn, với đúng những file không đổi nội dung. Khoá cache của COPY là nội dung; hàng "chỉ touch" trong bảng đo cho kết quả CACHED. Bản build cũ trước BuildKit thì nhạy hơn với metadata, nên kinh nghiệm cũ hay nói ngược lại.
    Dockerfile chỉ có COPY package.json . và COPY src/ ./src. Thêm .dockerignore có làm build nhanh hơn không? Gần như không, về lượng dữ liệu gửi đi: đo được 37 byte so với 27 byte cho trường hợp COPY từng file. Nó chỉ có tác dụng lớn khi Dockerfile có COPY ., nơi đo được 15,73 MB xuống 107 byte.
    Vì sao thêm .dockerignore lại làm vỡ cache một lần? Vì tập file mà COPY . chép vừa đổi, nên đầu vào của bước đó đổi theo. Đó là một lần vỡ duy nhất; từ lần build sau, mọi thay đổi trong thư mục đã bị bỏ đều không còn đụng tới cache.