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.

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:

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 theoRUNcài deps chạy lại (sleep 3). - Bản ĐÚNG: sau khi đổi code, build chỉ 0,6s —
RUNcài deps vẫn được cache (vìrequirements.txtkhông đổi), chỉ mỗi layerCOPY . .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ề
- 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.
- 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.txttrước) chỉ 0,6s — nhanh ~6× dù cùng app. - 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
- Docker docs — Best practices for writing Dockerfiles / Leverage build cache: https://docs.docker.com/build/cache/
- Docker docs — Dockerfile reference: https://docs.docker.com/reference/dockerfile/
- Docker docs — Cache mounts (BuildKit): https://docs.docker.com/build/cache/optimize/
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.