Pipeline Của Bạn Đang Chậm — Và Nó Tốn Kém Hơn Bạn Nghĩ
Bạn push một thay đổi config nhỏ. Có thể chỉ là cập nhật một biến môi trường hay sửa lỗi đánh máy trong comment của Dockerfile. Rồi bạn ngồi chờ. Năm phút. Tám phút. Đôi khi mười phút. Mọi kỹ sư trong team đều gặp phải cái bức tường đó, nhiều lần trong ngày.
Tôi đã trải qua điều này. Ở một dự án tôi tham gia, CI pipeline đang rebuild toàn bộ Docker image từ đầu ở mỗi commit — tải lại tất cả dependencies npm, biên dịch lại các native module, mọi thứ. Không ai thiết lập caching vì “nó chạy được, thôi kệ.” Sau khi chúng tôi thêm Remote Caching và BuildKit, thời gian build trung bình giảm từ 9 phút xuống dưới 90 giây. Đó là loại thay đổi khiến developer thực sự thích push code trở lại.
Hướng dẫn này sẽ chỉ cho bạn cách làm điều đó — từ việc hiểu tại sao build chậm cho đến cách kết nối remote cache hoạt động trong pipeline của bạn.
Khái Niệm Cốt Lõi: Điều Gì Thực Sự Xảy Ra Khi Build Docker
Layer Caching và Tại Sao Nó Bị Vỡ Trong CI
Docker build hoạt động theo từng layer. Mỗi lệnh trong Dockerfile (RUN, COPY, ADD) tạo ra một layer mới. Docker cache các layer này trên đĩa — vì vậy nếu không có gì thay đổi ở layer đó hoặc bất kỳ layer nào trước nó, Docker bỏ qua việc chạy lại lệnh và dùng kết quả đã cache.
Vấn đề là: hầu hết các CI runner đều là ephemeral. Mỗi lần GitHub Actions hay GitLab CI khởi động một runner, nó bắt đầu với một slate sạch. Không có local cache. Mọi build đều là cold build, và Docker làm lại mọi thứ từ đầu.
Docker BuildKit Mang Lại Điều Gì
BuildKit là build engine thế hệ tiếp theo của Docker, có sẵn từ Docker 18.09. Nó có nhiều ưu điểm so với build engine cổ điển:
- Thực thi song song các build stage độc lập
- Quản lý cache tốt hơn và kiểm soát cache chi tiết hơn
- Hỗ trợ export và import cache từ remote storage
- Secrets và SSH forwarding mà không nhúng credentials vào layer
Tính năng chúng ta quan tâm ở đây là cache export/import — cụ thể là đẩy build cache lên registry và kéo lại ở lần chạy tiếp theo.
Remote Caching: Mảnh Ghép Còn Thiếu
Remote caching có nghĩa là lưu trữ Docker build cache ở một nơi bền vững — thường là container registry như Docker Hub, Amazon ECR, hoặc GitHub Container Registry (GHCR). Khi CI runner bắt đầu build mới, nó tải cache đó về trước khi build. Các layer không thay đổi được bỏ qua hoàn toàn.
BuildKit hỗ trợ nhiều cache backend:
- registry — Lưu cache dưới dạng OCI image layer trong container registry (phổ biến nhất)
- local — Lưu cache trên filesystem cục bộ (hữu ích cho self-hosted runner với persistent volume)
- s3 — Lưu cache trong S3-compatible bucket (thông qua external cache driver của BuildKit)
- gha — Cache native của GitHub Actions (không cần registry, tích hợp chặt chẽ)
Thực Hành: Kết Nối Tất Cả Lại
Bước 1 — Bật BuildKit
Đầu tiên, hãy chắc chắn BuildKit đã được bật. Docker 23.0+ bật mặc định. Với các phiên bản cũ hơn, đặt biến môi trường:
export DOCKER_BUILDKIT=1
Hoặc đặt vĩnh viễn trong Docker daemon config (/etc/docker/daemon.json):
{
"features": {
"buildkit": true
}
}
Bước 2 — Viết Dockerfile Thân Thiện Với Cache
Trước khi đụng vào remote caching, hãy sắp xếp cấu trúc Dockerfile đúng cách. Quy tắc đơn giản là: đặt những thứ ít thay đổi hơn lên đầu file.
Đây là ứng dụng Node.js điển hình với thứ tự layer tệ:
# Tệ: copy source code trước khi cài dependencies
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]
Mỗi lần bạn thay đổi một file source, layer COPY . . bị invalidate. Điều đó có nghĩa là npm install chạy lại từ đầu — mỗi lần một.
Cách sửa: tách việc cài dependency ra khỏi phần copy source.
# Tốt: dependencies được cache riêng khỏi source code
FROM node:20-alpine
WORKDIR /app
# Copy package file trước
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Source code thay đổi ở đây, nhưng npm ci đã được cache rồi
COPY . .
CMD ["node", "index.js"]
Bây giờ npm ci chỉ chạy lại khi package.json hoặc package-lock.json thay đổi. Việc sửa file source không đụng đến dependency layer.
Bước 3 — Build với Cache Export
Dùng flag --cache-to và --cache-from của BuildKit để đẩy và kéo cache từ registry. Thay your-registry/your-image bằng đường dẫn image thực tế của bạn:
# Build và export cache lên registry
docker buildx build \
--cache-from type=registry,ref=your-registry/your-image:cache \
--cache-to type=registry,ref=your-registry/your-image:cache,mode=max \
-t your-registry/your-image:latest \
--push \
.
Hai flag cần hiểu:
--cache-from— kéo cache hiện có từ registry trước khi build--cache-to mode=max— export tất cả các layer trung gian vào cache, không chỉ image cuối cùng. Lưu nhiều dữ liệu hơn, nhưng tái sử dụng tối đa ở lần build tiếp theo.
Bước 4 — Tích Hợp GitHub Actions (Ví Dụ Đầy Đủ)
Đây là workflow GitHub Actions hoàn chỉnh dùng GHCR làm cache backend:
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
Build đầu tiên luôn là cold — chưa có cache nào, nên nó chạy đủ thời gian. Từ build thứ hai trở đi, các layer không thay đổi được lấy thẳng từ registry. Bạn sẽ thấy sự khác biệt ngay lập tức.
Bước 5 — Thay Thế: GitHub Actions Cache Backend
Không muốn quản lý registry chỉ để lưu cache? GitHub Actions có sẵn cache backend tích hợp. Không cần thông tin xác thực registry cho tầng cache:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Backend gha dùng bộ nhớ cache tích hợp của GitHub — 10 GB mỗi repo. Thiết lập đơn giản hơn, nhưng có hai đánh đổi: cache bị xóa sau 7 ngày không hoạt động, và có thể chậm hơn caching dựa trên registry với image lớn.
Bước 6 — Multi-Stage Build để Kết Quả Còn Nhanh Hơn
Kết hợp remote caching với multi-stage build: image cuối nhỏ hơn, tái sử dụng cache tối đa.
# Stage 1: Cài dependencies (được cache tích cực)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage 2: Build ứng dụng
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production image (chỉ giữ artifact runtime)
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Với mode=max, BuildKit cache cả ba stage riêng biệt. Bạn thay đổi source code nhưng không đổi package.json? Stage deps được kéo từ cache và npm ci không bao giờ chạy.
Kiểm Tra Cache Có Hoạt Động Không
Theo dõi output của build. Các layer đã được cache trông như thế này:
#5 CACHED
#6 CACHED
#7 [builder 3/4] RUN npm run build 0.3s # Chỉ stage này mới chạy
Các bước được đánh dấu CACHED đã được kéo từ remote cache và bỏ qua hoàn toàn. Đó là lý do thời gian giảm từ 9 phút xuống còn 90 giây.
Con Số Thực Tế: Kỳ Vọng Gì
Kết quả thay đổi tùy theo kích thước image và tần suất thay đổi dependency. Nhưng qua một số dự án Node.js và Python tôi đã làm, mô hình luôn nhất quán:
- Cold build (không có cache): 5–12 phút cho ứng dụng Node.js hay Python thông thường
- Warm build (chỉ thay đổi source): 30–90 giây
- Warm build (thay đổi dependency): 2–4 phút (chỉ stage dependency rebuild lại)
Lợi ích lớn nhất đến từ các dự án có quá trình cài dependency nặng — bất cứ thứ gì mà chỉ riêng npm install, pip install -r requirements.txt, hoặc go mod download đã mất hơn 60 giây. Đó chính là bước mà remote caching loại bỏ ở hầu hết các commit.
Triển Khai Thôi: Checklist Năm Bước
CI pipeline chậm không chỉ lãng phí thời gian — chúng phá vỡ sự tập trung, làm chậm phản hồi, và âm thầm xói mòn thói quen push commit nhỏ, thường xuyên. Remote caching với Docker BuildKit là một trong những thay đổi có ROI cao nhất bạn có thể làm cho pipeline, và cần ít code đến ngạc nhiên.
Đây là checklist:
- Cấu trúc lại Dockerfile để cài dependencies trước khi copy source code
- Bật Docker Buildx trong môi trường CI
- Thêm flag
cache-fromvàcache-totrỏ đến registry hoặc GHA cache - Dùng
mode=maxđể cache tất cả các layer trung gian - Cân nhắc multi-stage build để giữ image cuối nhỏ gọn
Một khi đã có tất cả, hầu hết các commit build xong trong dưới hai phút. Những cái lâu hơn là những lúc thứ gì đó nặng thực sự thay đổi — và đó chính xác là cách mọi thứ nên hoạt động.

