Lời khuyên quen thuộc: thêm .dockerignore để khỏi gửi hàng trăm megabyte cho Docker daemon mỗi lần build. Tôi dựng một dự án 28 MB đúng kiểu thật — node_modules 7,8 MB, .git 10 MB, dist 9,6 MB — rồi đo.

Kết quả không giống lời khuyên.

BuildKit chỉ gửi thứ nó cần

Dockerfile chỉ lấy hai thứ:

FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
COPY src /app/src
KHONG co .dockerignore : transferring context: 150B
CO    .dockerignore    : transferring context: 150B

150 byte, cả hai. Thư mục dự án 28 MB, nhưng BuildKit đọc Dockerfile trước, thấy chỉ cần requirements.txtsrc/, và chỉ lấy chừng đó. .dockerignore không thay đổi gì vì chẳng có gì để loại.

Đây là khác biệt so với builder cũ, thứ đóng gói toàn bộ thư mục thành một tar rồi gửi đi trước khi đọc Dockerfile. Lời khuyên kia viết cho thời đó. BuildKit là mặc định từ Docker 23, nên với đa số người đọc bài này thì tiền đề của lời khuyên đã không còn.

Trừ khi bạn viết COPY . /app

Đổi Dockerfile sang copy hết:

Dockerfile .dockerignore Context truyền Thời gian build
COPY src không 150 B 7,4 s
COPY src 150 B 7,1 s
COPY . không 28,06 MB 7,2 s
COPY . 599 B 6,9 s

Với COPY . /app, .dockerignore cắt context từ 28,06 MB xuống 599 B — gấp gần 47 000 lần.

Nhưng nhìn cột cuối: thời gian build gần như không đổi (7,2 so với 6,9 giây). Chuyển 28 MB giữa hai tiến trình trên cùng một máy gần như miễn phí. Nếu bạn thêm .dockerignore để build nhanh hơn, phép đo này nói rằng bạn sẽ thất vọng.

Với builder ở xa — BuildKit chạy trên máy khác, hoặc CI kéo context qua mạng — con số 28 MB kia sẽ hiện ra thành thời gian thật. Nhưng trên máy của bạn thì không.

Cái giá thật thứ nhất: image béo lên

COPY . /app không chỉ gửi 28 MB, nó đưa chúng vào image:

Dung lượng
COPY . không có .dockerignore 42,5 MB
COPY ..dockerignore 23,4 MB

Chênh 19,1 MB, và nhìn vào bên trong thì thấy rõ nó là gì:

9.6M  /app/dist
9.4M  /app/node_modules
8.0K  /app/src
4.0K  /app/requirements.txt

Mã nguồn thật của dự án là 8 KB. Phần còn lại là kết quả build cũ và thư mục phụ thuộc của một hệ sinh thái khác, được chép vào image, đẩy lên registry, tải về mọi máy chủ, và không bao giờ dùng tới.

Còn tệ hơn thế: nếu .git lọt vào image thì toàn bộ lịch sử kho mã đi cùng — kể cả những commit đã xoá tệp bí mật, vì xoá trong commit sau không xoá được trong lịch sử.

Cái giá thật thứ hai: cache chết liên tục

Đây mới là lý do đáng thêm .dockerignore, và nó không xuất hiện trong bất kỳ phép đo về dung lượng nào.

Tôi sửa một tệp trong node_modules — thứ mà Dockerfile chẳng dùng tới — rồi build lại:

Không sửa gì Sau khi sửa node_modules
COPY . không.dockerignore 3 bước CACHED 1 bước CACHED
COPY . .dockerignore 3 bước CACHED 3 bước CACHED

Không có .dockerignore, cache tụt từ 3 xuống 1 — nghĩa là pip install chạy lại từ đầu, dù không một phụ thuộc nào thay đổi.

Và hãy nghĩ xem những thư mục đó bị ghi vào lúc nào:

  • .git — mỗi lần bạn git commit, git checkout, hay thậm chí git status trên vài trình Git.
  • node_modules — mỗi lần trình soạn thảo chạy công cụ ngôn ngữ.
  • dist, target, build — mỗi lần bạn build ở ngoài container.
  • .venv, __pycache__ — mỗi lần bạn chạy test.

Tức là gần như mọi thao tác bình thường trong ngày đều phá cache build của bạn, và bạn sẽ kết luận rằng "Docker build lúc nào cũng chậm" mà không biết vì sao.

Đây là điểm nối với phần 6: ở đó tôi đo được rằng đặt COPY đúng chỗ tiết kiệm tám lần thời gian build. .dockerignore là nửa còn lại của cùng một câu chuyện — sắp xếp đúng thứ tự mà vẫn để .git lọt vào context thì cache vẫn chết như thường.

.dockerignore nên có gì

Danh sách tối thiểu cho gần như mọi dự án:

.git
.gitignore
node_modules
__pycache__
*.pyc
.venv
target
dist
build
.env
*.log
.DS_Store
Dockerfile*
.dockerignore

Hai dòng cuối hay bị bỏ qua và đáng có: chính Dockerfile không cần nằm trong image, và sửa nó thì không nên phá cache của bước COPY.

Dòng .env thì quan trọng vì lý do khác hẳn: COPY . /app sẽ chép thẳng tệp bí mật của bạn vào image, và như phần 5 đã đo, thứ gì vào image thì ai kéo được image cũng đọc được.

Cú pháp giống .gitignore nhưng không phải là nó — chúng là hai tệp riêng, và Docker không đọc .gitignore. Có một khác biệt đáng nhớ: dấu ! để loại trừ ngược hoạt động, nhưng thứ tự dòng quyết định, và loại trừ ngược không "cứu" được tệp nằm trong thư mục đã bị loại.

Cách viết an toàn hơn cả .dockerignore

Nhìn lại bảng đầu bài: dòng COPY src cho 150 B mà không cần .dockerignore gì cả.

Chép có chọn lọc thì bạn không phải nhớ loại trừ những gì:

COPY requirements.txt ./
COPY src/ ./src/

thay vì

COPY . .

Vẫn nên có .dockerignore làm lưới an toàn — vì sớm muộn ai đó cũng thêm một dòng COPY . — nhưng liệt kê tường minh thứ cần chép là cách duy nhất khiến một tệp mới xuất hiện trong dự án không tự động chui vào image.

Thử ba mươi giây

Xem build của bạn đang gửi bao nhiêu, và image đang chứa những gì không cần:

docker build --progress=plain -t thu . 2>&1 | grep 'transferring context'
docker run --rm thu du -sh /app/* 2>/dev/null | sort -rh | head

Nếu dòng đầu báo hàng chục megabyte, Dockerfile của bạn có COPY . và thiếu .dockerignore. Nếu dòng thứ hai liệt kê node_modules, dist hay .git, chúng đang được tải về mọi máy chủ mỗi lần triển khai.

Phần sau mổ ENTRYPOINTCMD: bốn tổ hợp, và tổ hợp nào làm container của bạn không dừng được.