Docker Build Cache & Layering
Bạn đã bao giờ tự hỏi tại sao lần build đầu tiên mất 5-10 phút, nhưng lần build thứ hai chỉ mất vài giây? Đó là nhờ Build Cache. Nhưng đôi khi, chỉ thay đổi 1 dòng code mà Docker lại rebuild toàn bộ — đó là vì bạn chưa hiểu cách Layering hoạt động. 🧱
Trong bài viết này, tôi sẽ giải thích chi tiết cơ chế Layer và Build Cache, cách Docker quyết định khi nào dùng cache và khi nào rebuild, cùng các kỹ thuật tối ưu để build nhanh hơn.
Layer trong Docker là gì?
Mỗi lệnh trong Dockerfile tạo ra một layer (lớp) trong image. Bạn có thể hình dung image như một chồng các lớp xếp chồng lên nhau, mỗi lớp thêm nội dung mới lên trên lớp trước đó.
Cách Layer được tạo ra
| |
| Layer | Lệnh Dockerfile | Nội dung |
|---|---|---|
| Layer 1 | FROM node:18-alpine | Base OS + Node.js runtime |
| Layer 2 | WORKDIR /app | Tạo và chuyển vào thư mục /app |
| Layer 3 | COPY package.json ./ | Thêm file package.json |
| Layer 4 | RUN npm install | Cài đặt node_modules |
| Layer 5 | COPY . . | Thêm toàn bộ source code |
| Layer 6 | RUN npm run build | Tạo file build (dist/) |
Đặc điểm quan trọng của Layer
- Read-only: Mỗi layer sau khi tạo xong là chỉ đọc, không thể thay đổi
- Chia sẻ được: Nhiều image có thể dùng chung các layer giống nhau (ví dụ: cùng base image
node:18-alpine) - Có thứ tự: Layer sau được xếp chồng lên layer trước, tạo thành file system hoàn chỉnh
- Có kích thước: Mỗi layer có dung lượng riêng, tổng tất cả layer = kích thước image
Build Cache hoạt động như thế nào?
Build Cache là cơ chế giúp Docker bỏ qua các bước không cần thiết khi rebuild image. Nếu một layer không thay đổi so với lần build trước, Docker sẽ dùng lại kết quả cũ thay vì chạy lại.
Quy tắc cache của Docker
Docker kiểm tra cache theo thứ tự từng lệnh trong Dockerfile:
| |
Quy tắc quan trọng nhất: Khi một layer bị invalidate (mất cache), tất cả các layer phía sau đều phải rebuild — kể cả khi chúng không thay đổi gì.
Khi nào cache bị invalidate?
| Trường hợp | Cache bị mất? | Giải thích |
|---|---|---|
| Thay đổi lệnh trong Dockerfile | ✅ Có | Docker so sánh chuỗi lệnh, khác → rebuild |
File trong COPY/ADD thay đổi | ✅ Có | Docker tính checksum của file, khác → rebuild |
Chỉ thay đổi mtime của file | ❌ Không | Docker không xét thời gian sửa đổi, chỉ xét nội dung |
Lệnh RUN giống hệt lần trước | ❌ Không | Cùng chuỗi lệnh → dùng cache (kể cả kết quả đã cũ) |
| Layer trước đó bị invalidate | ✅ Có | Tất cả layer phía sau đều phải rebuild |
Ví dụ minh hoạ cache invalidation
| |
Chỉ sửa 1 file .js nhỏ, nhưng npm install phải chạy lại dù package.json không đổi. Đây là vấn đề phổ biến nhất khi viết Dockerfile chưa tối ưu.
Kỹ thuật tối ưu Build Cache
Sắp xếp lệnh theo tần suất thay đổi
Nguyên tắc: Lệnh ít thay đổi → đặt trước. Lệnh hay thay đổi → đặt sau.
❌ Chưa tối ưu:
| |
✅ Đã tối ưu:
| |
Kết quả: Khi chỉ sửa source code, npm install được cache lại → tiết kiệm 2-5 phút mỗi lần build.
Sử dụng .dockerignore
File .dockerignore giúp loại bỏ các file không cần thiết khỏi build context, giảm nguy cơ cache bị invalidate vô ích.
| |
Tại sao quan trọng? Nếu không có .dockerignore, lệnh COPY . . sẽ copy cả node_modules (hàng trăm MB) và .git vào build context. Bất kỳ thay đổi nào trong các thư mục này đều làm cache miss.
Sử dụng Cache Mounts
Cache mounts cho phép lưu trữ cache của package manager giữa các lần build, kể cả khi layer bị rebuild.
Node.js:
| |
Python:
| |
Go:
| |
Lợi ích: Ngay cả khi package.json thay đổi và layer phải rebuild, các package đã tải trước đó vẫn được lưu trong cache mount → chỉ tải package mới, không tải lại toàn bộ.
Multi-stage Build
Multi-stage build giúp tách quá trình build và runtime, giảm kích thước image cuối cùng và tối ưu cache cho từng giai đoạn.
| |
| Stage | Mục đích | Kích thước | Có trong image cuối? |
|---|---|---|---|
| builder | Build ứng dụng | ~500 MB | ❌ Không |
| production | Chạy ứng dụng | ~30 MB | ✅ Có |
Ví dụ thực tế: Tối ưu Dockerfile cho Node.js
Dockerfile chưa tối ưu
| |
Vấn đề:
- ❌ Base image lớn (
node:18~ 900 MB) - ❌
COPY . .trướcnpm install→ mọi thay đổi code đều rebuild dependencies - ❌ Không có
.dockerignore→ copy cảnode_modules,.git - ❌ Không dùng multi-stage → image cuối chứa devDependencies
Dockerfile đã tối ưu
| |
Cải thiện:
- ✅ Base image nhỏ (
node:18-alpine~ 170 MB) - ✅ Tách
COPY package.jsonriêng → cache dependencies hiệu quả - ✅ Cache mount cho npm → tải lại nhanh khi rebuild
- ✅ Multi-stage → image cuối chỉ chứa production dependencies
- ✅ Sử dụng
npm cithaynpm install→ đảm bảo reproducible builds
So sánh kết quả
| Tiêu chí | Chưa tối ưu | Đã tối ưu |
|---|---|---|
| Kích thước image | ~900 MB | ~170 MB |
| Build lần đầu | ~5 phút | ~5 phút |
| Rebuild khi đổi code | ~5 phút | ~30 giây |
| Rebuild khi đổi deps | ~5 phút | ~2 phút |
Các lệnh hữu ích để debug Cache
| |
Đọc output build: Khi build, Docker hiển thị CACHED cho các layer dùng cache:
| |
Các dòng có CACHED nghĩa là layer đó được lấy từ cache, không cần rebuild.
Ghi chú triển khai
- Khi áp dụng vào dự án của bạn, hãy lưu ý các điểm quan trọng:
- Luôn tạo file
.dockerignoretrước khi viết Dockerfile - Tách
COPY package.jsonriêng trướcRUN npm install - Sử dụng multi-stage build cho production image
- Cache mount (
--mount=type=cache) cần BuildKit (mặc định từ Docker 23.0+)
- Luôn tạo file
- Best practices:
- Sắp xếp lệnh: ít thay đổi → trước, hay thay đổi → sau
- Dùng
npm cithaynpm installtrong CI/CD - Pin version cụ thể cho base image (
node:18.12-alpinethay vìnode:latest)
- Troubleshooting:
- Build chậm dù không đổi gì? → Kiểm tra
.dockerignore, có thể file không liên quan đang trigger cache miss - Cache mount không hoạt động? → Đảm bảo Docker BuildKit đang bật:
DOCKER_BUILDKIT=1 - Muốn xem layer nào bị miss? → Dùng
docker build --progress=plain
- Build chậm dù không đổi gì? → Kiểm tra
🎯 Lời kết
Tóm tắt lại:
- Layer = mỗi lệnh trong Dockerfile tạo ra một lớp trong image
- Build Cache = Docker lưu lại kết quả các layer để dùng lại khi rebuild
- Cache invalidation = khi một layer thay đổi, tất cả layer phía sau đều phải rebuild
- Tối ưu = sắp xếp lệnh hợp lý +
.dockerignore+ cache mounts + multi-stage build
Chỉ cần hiểu và áp dụng 4 kỹ thuật trên, bạn có thể giảm thời gian build Docker từ 5 phút xuống dưới 30 giây cho phần lớn trường hợp. 🚀
Nếu bạn thấy bài viết này hữu ích, hãy nhớ ⭐ star repo hoặc 📱 chia sẻ với bạn bè nhé! 😊
