Featured image of post Docker Build Cache & Layering

Docker Build Cache & Layering

Hiểu rõ cơ chế Layer và Build Cache trong Docker - tại sao build chậm và cách tối ưu để build nhanh hơn.

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

1
2
3
4
5
6
FROM node:18-alpine        # Layer 1: Base image
WORKDIR /app               # Layer 2: Tạo thư mục làm việc
COPY package.json ./       # Layer 3: Copy file package.json
RUN npm install            # Layer 4: Cài dependencies
COPY . .                   # Layer 5: Copy source code
RUN npm run build          # Layer 6: Build ứng dụng
LayerLệnh DockerfileNội dung
Layer 1FROM node:18-alpineBase OS + Node.js runtime
Layer 2WORKDIR /appTạo và chuyển vào thư mục /app
Layer 3COPY package.json ./Thêm file package.json
Layer 4RUN npm installCài đặt node_modules
Layer 5COPY . .Thêm toàn bộ source code
Layer 6RUN npm run buildTạ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:

1
2
3
4
5
Lệnh 1 → Có cache? ✅ Dùng lại → Tiếp tục
Lệnh 2 → Có cache? ✅ Dùng lại → Tiếp tục
Lệnh 3 → Có cache? ❌ Cache miss → Rebuild từ đây trở đi
Lệnh 4 → Bắt buộc rebuild (vì lệnh 3 đã thay đổi)
Lệnh 5 → Bắt buộc rebuild

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ợpCache 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ôngDocker 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ôngCù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

1
2
3
4
5
FROM node:18-alpine          # ✅ Cache hit (không đổi)
WORKDIR /app                 # ✅ Cache hit (không đổi)
COPY . .                     # ❌ Cache miss (bạn sửa 1 file .js)
RUN npm install              # ❌ Phải rebuild (vì COPY ở trên miss)
RUN npm run build            # ❌ Phải rebuild (vì layer trước miss)

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:

1
2
3
4
5
FROM node:18-alpine
WORKDIR /app
COPY . .                     # Copy tất cả (thay đổi thường xuyên)
RUN npm install              # Phải chạy lại mỗi khi code thay đổi
RUN npm run build

✅ Đã tối ưu:

1
2
3
4
5
6
FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./    # Ít thay đổi → copy trước
RUN npm install                   # Chỉ rebuild khi dependencies đổi
COPY . .                          # Hay thay đổi → copy sau
RUN npm run build

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.

1
2
3
4
5
6
7
8
# .dockerignore
node_modules
dist
.git
.env
*.log
tmp*
.DS_Store

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:

1
2
3
4
5
6
7
FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/root/.npm \
    npm install
COPY . .
RUN npm run build

Python:

1
2
3
4
5
6
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY . .

Go:

1
2
3
4
5
6
7
8
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go mod download
COPY . .
RUN go build -o /app/server

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Stage 1: Build (layer nặng nhưng không nằm trong image cuối)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Runtime (chỉ chứa file cần thiết)
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
StageMục đíchKích thướcCó trong image cuối?
builderBuild ứng dụng~500 MB❌ Không
productionChạ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

1
2
3
4
5
6
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]

Vấn đề:

  • ❌ Base image lớn (node:18 ~ 900 MB)
  • COPY . . trước npm 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Stage 1: Install dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# Stage 2: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build

# Stage 3: Runtime
FROM node:18-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

EXPOSE 3000
CMD ["node", "dist/server.js"]

Cải thiện:

  • ✅ Base image nhỏ (node:18-alpine ~ 170 MB)
  • ✅ Tách COPY package.json riê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 ci thay npm 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Xem chi tiết quá trình build (hiển thị cache hit/miss)
docker build --progress=plain -t myapp .

# Build không dùng cache (force rebuild toàn bộ)
docker build --no-cache -t myapp .

# Chỉ invalidate cache cho stage cụ thể
docker build --no-cache-filter builder -t myapp .

# Xem các layer của image
docker history myapp

# Xem dung lượng build cache
docker builder du

# Xoá build cache
docker builder prune

Đọc output build: Khi build, Docker hiển thị CACHED cho các layer dùng cache:

1
2
3
4
 => CACHED [deps 2/3] COPY package.json package-lock.json ./
 => CACHED [deps 3/3] RUN npm ci --only=production
 => [builder 4/5] COPY . .
 => [builder 5/5] RUN npm run build

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 .dockerignore trước khi viết Dockerfile
    • Tách COPY package.json riêng trước RUN 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+)
  • Best practices:
    • Sắp xếp lệnh: ít thay đổi → trước, hay thay đổi → sau
    • Dùng npm ci thay npm install trong CI/CD
    • Pin version cụ thể cho base image (node:18.12-alpine thay 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

🎯 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é! 😊


Tài liệu tham khảo