Bạn sửa đúng một dòng trong app.py, chạy docker build, và... nó ngồi cài lại toàn bộ dependencies mất mấy phút. Nghe quen? Đây là lỗi Dockerfile phổ biến nhất, và nó không phải do Docker chậm — mà do thứ tự các lệnh trong Dockerfile. Bài trước ta thấy image là chồng layer; bài này (phần 7 loạt Docker) cho thấy các layer đó được cache thế nào khi build, vì sao một thay đổi nhỏ có thể làm hỏng cache của tất cả, và cách sắp lệnh để build lại gần như tức thì — đo bằng thời gian thật.

Cache layer: đổi một, hỏng cả chuỗi sau

Khi docker build, mỗi lệnh (RUN, COPY...) tạo một layer, và Docker cache layer đó. Lần build sau, nó so từng lệnh với cache:

  • Lệnh và input không đổi → dùng lại layer cache, bỏ qua việc chạy (rất nhanh).
  • Lệnh hoặc input đổi → layer đó và mọi layer sau nó phải build lại từ đầu.

Vế thứ hai là mấu chốt: cache có tính tuần tự. Một layer bị miss thì tất cả layer phía dưới nó cũng miss — dù chúng không hề đổi. Vì thế vị trí của lệnh hay đổi quyết định bao nhiêu công việc bị lặp lại.

Lỗi kinh điển: COPY . . trước khi cài deps

FROM alpine
RUN apk add python3
COPY . /app          # <- code hay đổi, layer này hay bị miss
RUN pip install ...  # <- nằm SAU COPY, nên đổi code là cài lại deps

COPY . /app copy toàn bộ source. Source code đổi liên tục (mỗi lần sửa), nên layer này miss liên tục — và vì RUN pip install nằm sau nó, deps bị cài lại mỗi lần bạn sửa một dòng code. Cực kỳ lãng phí, vì deps hầu như không đổi.

Ảnh chụp đoạn mã nền tối minh hoạ Dockerfile cache thứ tự lệnh quyết định build nhanh hay chậm, mỗi lệnh một layer layer được cache theo nội dung build lại docker so từng lệnh với cache nếu lệnh cộng input không đổi dùng lại layer cache bỏ qua chạy nếu đổi layer đó và mọi layer sau nó bị build lại từ đầu một layer đổi làm hỏng cache của tất cả layer phía sau, sai COPY chấm chấm trước khi cài dependencies FROM alpine RUN apk add python3 COPY chấm app code hay đổi layer này hay miss RUN pip install nằm sau COPY đổi code là cài lại deps sửa 1 dòng code build lại toàn bộ deps chậm, đúng COPY file phụ thuộc trước cài deps rồi mới COPY code FROM alpine RUN apk add python3 COPY requirements.txt app chỉ đổi khi deps đổi hiếm RUN pip install cache khi requirements không đổi COPY chấm app code đổi chỉ miss từ đây sửa code deps vẫn cache build gần như tức thì, nguyên tắc xếp từ ít đổi tới hay đổi base image hệ thống dependencies source code thứ hay đổi nhất code để cuối miss ít layer nhất

Hình 1: Cache layer có tính tuần tự — một layer miss thì mọi layer sau miss. Đặt COPY . . trước RUN pip install khiến đổi code làm cài lại deps; đặt COPY requirements.txt trước rồi mới COPY . . giữ được cache deps.

Cách đúng và đo thật

Sắp lại: copy chỉ file khai báo phụ thuộc trước, cài deps, rồi mới copy source:

FROM alpine
RUN apk add python3
COPY requirements.txt /app/   # chỉ đổi khi deps đổi (hiếm)
RUN pip install ...           # cache khi requirements không đổi
COPY . /app                   # code đổi chỉ miss từ đây trở đi

Mình dựng hai Dockerfile cùng một app (deps giả lập cài mất ~3s) và đo thời gian build:

Ảnh chụp bảng kết quả chạy thật docker build cache output thật deps giả lập cài mất 3s đo thời gian build, một sai COPY chấm chấm trước deps build lần 1 chưa cache 11.7s build lần 2 không đổi gì 0.6s cache toàn bộ build lần 3 đổi code 3.8s COPY miss cài lại deps, hai đúng COPY requirements trước rồi COPY code build lần 1 chưa cache 3.7s build lần 2 đổi code 0.6s deps vẫn cache chỉ COPY code miss, ba so sánh khi sửa code việc làm hằng ngày sai 3.8s cài lại deps mỗi lần sửa code đúng 0.6s nhanh hơn 6x chỉ copy lại code cùng một app chỉ khác thứ tự lệnh trong Dockerfile, kết đổi 1 layer làm miss mọi layer sau xếp thứ ít đổi deps trước thứ hay đổi code giữ cache phần đắt nhất

Hình 2: Chạy thật — bản SAI: build đầu 11,7s, không đổi gì 0,6s, đổi code 3,8s (cài lại deps); bản ĐÚNG: build đầu 3,7s, đổi code 0,6s (deps vẫn cache). Khi sửa code, đúng nhanh hơn ~6× dù cùng một app.

  • Bản SAI: sau khi đổi code, build mất 3,8s — vì COPY . . miss kéo theo RUN cài deps chạy lại (sleep 3).
  • Bản ĐÚNG: sau khi đổi code, build chỉ 0,6s — RUN cài deps vẫn được cache (vì requirements.txt không đổi), chỉ mỗi layer COPY . . cuối bị miss.

Cùng một ứng dụng, cùng dependencies — khác biệt duy nhất là thứ tự lệnh, và nó biến build hằng ngày từ 3,8s xuống 0,6s (nhanh ~6×). Trên dự án thật với deps nặng (npm install, pip nhiều gói), khác biệt là hàng phút mỗi lần build.

Nguyên tắc: xếp từ ít đổi tới hay đổi

Quy tắc chung để tối ưu cache: sắp các lệnh theo tần suất thay đổi tăng dần.

base image  →  cài gói hệ thống  →  cài dependencies  →  copy source code
(ít đổi nhất)                                            (hay đổi nhất)

Đặt thứ hay đổi nhất (source code) ở cuối để một thay đổi chỉ làm miss các layer cuối — giữ nguyên cache của phần đắt nhất (cài deps).

Đánh đổi cần cân nhắc

COPY requirements.txt trước là mẫu chuẩn cho mọi ngôn ngữ. Python (requirements.txt/pyproject.toml), Node (package.json+package-lock.json), Go (go.mod+go.sum), Java (pom.xml) — copy chỉ file khai báo phụ thuộc trước, cài, rồi copy phần còn lại. Đây là mẫu áp dụng ở gần như mọi Dockerfile production. Nhớ copy cả file lock để cache đúng theo phiên bản chốt.

Cache dựa trên checksum của input, không chỉ tên file. COPY tính cache theo nội dung file (checksum), không phải chỉ đường dẫn. Nên COPY requirements.txt chỉ miss khi nội dung file thực sự đổi. Nhưng RUN apt update thì Docker không biết nội dung Internet đổi — nó cache theo chuỗi lệnh, nên có thể dùng cache cũ; đây là lý do đôi khi cần --no-cache để lấy gói mới nhất.

BuildKit và cache mount cho tình huống nâng cao. Docker hiện đại (BuildKit) có RUN --mount=type=cache để giữ cache của trình quản lý gói (npm/pip cache) xuyên qua các build, kể cả khi layer bị rebuild. Với deps rất lớn, đây là bước tối ưu tiếp theo sau khi đã sắp đúng thứ tự lệnh. Bắt đầu bằng thứ tự lệnh đúng — nó miễn phí và hiệu quả nhất.

Ba ý mang về

  1. Cache layer có tính tuần tự: đổi một lệnh làm miss layer đó và mọi layer sau; vì thế vị trí của lệnh hay đổi quyết định bao nhiêu công việc bị lặp lại khi build.
  2. COPY code trước khi cài deps là lỗi đắt: đo thật, sau khi sửa code bản SAI (COPY . . trước) mất 3,8s vì cài lại deps, còn bản ĐÚNG (COPY requirements.txt trước) chỉ 0,6s — nhanh ~6× dù cùng app.
  3. Xếp lệnh từ ít đổi tới hay đổi: base → hệ thống → dependencies → source code; copy file khai báo phụ thuộc (kèm lock) trước để giữ cache phần cài deps đắt nhất — mẫu chuẩn cho mọi ngôn ngữ.

Nguồn

Phần sau ta xem một thứ ẩn làm build chậm và image phình to: build context — vì sao docker build gửi cả thư mục lên daemon, và .dockerignore cứu bạn khỏi việc gửi nhầm node_modules hay .git.