
Trả lời nhanh: Docker build chậm thường vì hai tội: Dockerfile xếp sai thứ tự khiến cache layer vô dụng (COPY toàn bộ code trước khi cài dependency = đổi một dòng code là cài lại cả thế giới), và ổ đĩa yếu, build là chuỗi ghi layer, bung file, snapshot liên tục. Sửa theo thứ tự: xếp lệnh từ ít-đổi đến hay-đổi (COPY package.json → cài → COPY . ), dùng --mount=type=cache cho kho gói, multi-stage cho image gọn, và đặt máy build trên ổ NVMe.
Ai build image lần đầu cũng sốc: sao đổi đúng một dòng code mà chờ như build lại từ đầu? Vì đúng là nó đang build lại từ đầu, do cách viết Dockerfile phá vỡ cơ chế cache tinh tế của Docker. Bài này giải thích cơ chế đó bằng hình dung đơn giản, kèm các mẫu Dockerfile chuẩn cho dự án Node và cách đo để biết thời gian đang trôi vào khúc nào.
- Mỗi lệnh Dockerfile = một layer cache; đổi một layer là mọi layer SAU nó build lại
- Nguyên tắc vàng: xếp từ ít-đổi (cài hệ, dependency) đến hay-đổi (code), cache sống lâu nhất
- --mount=type=cache giữ kho npm/pip qua các lần build, thay đổi lớn thứ hai sau thứ tự lệnh
- Build = I/O dữ dội: snapshot layer, bung vạn file, ổ NVMe rút ngắn mọi lần build, kể cả khi cache hụt
Cache layer hoạt động thế nào - hình dung 30 giây
Docker build chạy từng lệnh Dockerfile từ trên xuống, mỗi lệnh chụp một layer. Lần build sau, nó so từng lệnh: lệnh và đầu vào y hệt → dùng lại layer cũ (CACHED, tức thì); gặp lệnh đầu tiên có thay đổi → từ đó trở xuống làm lại tất cả. Nghĩa là thứ tự lệnh chính là chiến lược cache: thứ hay đổi nhất phải nằm dưới cùng.
Mẫu Dockerfile Node đúng bài
# SAI: doi 1 dong code la npm ci chay lai
# COPY . .
# RUN npm ci && npm run build
# DUNG: dependency tach rieng, cache song ben
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Ba kỹ thuật trong một khuôn: tách COPY package.json trước npm ci (đổi code không đụng layer cài); cache mount giữ ~/.npm giữa các build, kể cả khi layer cài phải chạy lại, gói đã tải sẵn trong kho; multi-stage, image cuối chỉ mang sản phẩm chạy, không vác node_modules dev, nhẹ đi nhiều lần (kéo/đẩy registry cũng nhanh theo).
Đo để biết thời gian trôi đi đâu
docker build --progress=plain -t app . 2>&1 | grep -E '^#[0-9]+ (DONE|CACHED)'Mỗi bước in CACHED hay DONE kèm số giây. Đọc kết quả: bước nào lẽ ra CACHED mà cứ DONE → thứ tự lệnh hoặc .dockerignore có vấn đề (file rác lọt vào COPY làm đổi checksum, nhớ ignore node_modules, .git, .next). Mọi bước đều DONE chậm dù không đổi gì → nhìn xuống tầng máy: ổ đĩa và CPU.
Tầng máy: vì sao cùng Dockerfile, máy này build 40 giây máy kia 4 phút
Mỗi RUN là: bung file, ghi hàng vạn file nhỏ, chụp snapshot overlay layer, chuỗi thao tác metadata + ghi ngẫu nhiên dày đặc xuống ổ. Máy ổ NVMe nuốt chuỗi này êm; ổ SATA hoặc storage mạng biến từng RUN thành chờ đợi. CPU góp phần ở khúc npm run build (dịch code). Hệ quả thực dụng: CI runner tự host trên VPS NVMe thường build nhanh gấp nhiều lần runner miễn phí dùng chung, và bạn kiểm soát được cache sống giữa các lần chạy thay vì bắt đầu lạnh mỗi build.
Checklist chốt
- .dockerignore có node_modules, .git, .next, dist, *.log
- COPY package*.json + cài dependency TRƯỚC, COPY . SAU
- --mount=type=cache cho /root/.npm (hoặc pip/composer tương ứng)
- Multi-stage: image chạy không chứa đồ build
- Đo bằng --progress=plain trước và sau khi sửa
- Build trên máy ổ nhanh; CI lặp nhiều lần/ngày → cân runner tự host NVMe
Đo chính xác thời gian trôi vào đâu
docker build --progress=plain -t app . 2>&1 | grep -E '^#[0-9]+ (DONE|CACHED)' docker system df # dung luong image, container, build cache docker buildx du --verbose # chi tiet tung muc build cache docker builder prune --filter until=168h # don cache cu hon 7 ngay
Cách đọc: bước lẽ ra CACHED mà cứ DONE là thứ tự lệnh sai hoặc tệp rác lọt vào ngữ cảnh build. Mọi bước đều DONE chậm dù không đổi gì là chuyện tầng máy, tức là ổ đĩa và CPU.
Xem đúng những gì đang được gửi vào ngữ cảnh build: du -sh . so với dòng "transferring context" trong log. Chênh lệch lớn nghĩa là .dockerignore đang làm việc, giống nhau nghĩa là chưa.
Bốn thứ phá cache mà không ai ngờ
- Thư mục
.gitlọt vào ngữ cảnh. Mỗi commit là một lần đổi checksum, nênCOPY . .không bao giờ trúng cache. Thêm.gitvào.dockerignore. - Dấu thời gian của tệp. Một số công cụ sinh tệp có nhúng thời gian build vào nội dung, làm layer luôn mới. Kiểm bằng cách build hai lần liên tiếp không sửa gì, nếu vẫn DONE thì tìm tệp sinh tự động đó.
- Biến ARG đặt quá cao trong Dockerfile. Mọi layer nằm dưới một ARG có giá trị thay đổi đều mất cache. Đặt ARG ngay trên chỗ dùng nó, đừng đặt ở đầu tệp.
- Chạy build trên CI với runner mới mỗi lần. Cache nằm trên máy, máy mới là cache trống. Cách sửa ở mục dưới.
Giữ cache sống trên CI
Với runner dùng chung, mỗi lần chạy là môi trường lạnh. Hai cách giữ cache:
# 1. Day cache len registry, keo ve o lan build sau docker buildx build \ --cache-to type=registry,ref=registry.congty.vn/app:buildcache,mode=max \ --cache-from type=registry,ref=registry.congty.vn/app:buildcache \ -t registry.congty.vn/app:latest --push . # 2. Runner tu host: cache nam san tren o dia, khong can day di dau docker buildx build -t app .
Cách một chạy được ở mọi nền tảng CI nhưng tốn thời gian tải cache lên xuống, hợp khi image không quá lớn. Cách hai nhanh nhất vì cache nằm ngay trên ổ, đổi lại bạn phải tự quản máy build và nhớ dọn cache định kỳ bằng docker builder prune.
Hai kỹ thuật ít dùng nhưng hiệu quả
COPY --linktạo layer độc lập với các layer trước, nên đổi thứ tự hay sửa layer bên trên không làm layer này phải làm lại. Hữu ích với các bước chép tệp lớn ít đổi.- Chia cache mount theo dự án. Cú pháp
--mount=type=cache,target=/root/.npm,id=ten-du-angiữ kho gói riêng cho từng dự án, tránh việc nhiều dự án dùng chung một kho rồi tranh khóa khi build song song.
Nhắc lại một điều thực dụng: mọi kỹ thuật cache chỉ giúp khi cache trúng. Lần build đầu, lần đổi thư viện, lần dọn cache đều phải chạy thật, và khi đó tốc độ ổ đĩa quyết định. Đây là lý do cùng một Dockerfile chạy trên máy ổ NVMe nhanh hơn hẳn.
Câu hỏi thường gặp
Vì sao build trên CI chậm hơn trên máy tôi nhiều?
CI miễn phí thường: máy yếu dùng chung, và mỗi lần chạy là môi trường mới, cache layer lẫn kho npm đều lạnh. Giải pháp theo mức: bật cache của nền tảng CI, hoặc runner tự host trên VPS NVMe, cache sống, ổ nhanh, chủ động hoàn toàn.
.dockerignore quan trọng đến mức nào?
Rất: COPY . . mà không ignore là chép cả node_modules + .git vào context, vừa chậm khâu gửi context, vừa làm checksum đổi liên tục phá cache. File .dockerignore 5 dòng có khi cứu vài phút mỗi build.
BuildKit là gì, có cần bật không?
Là engine build hiện đại của Docker, bản mới đã mặc định. Các tính năng bài này dùng (--mount=type=cache, build song song stage) là đồ của BuildKit; nếu lệnh báo không hiểu cú pháp, bạn đang ở bản cũ, cập nhật Docker là xong.
Image của tôi 2 GB có sao không?
Chạy vẫn chạy, nhưng kéo/đẩy chậm và tốn chỗ registry. Multi-stage + base slim thường đưa app Node về 150-300 MB. Soi ai chiếm chỗ: docker history ten-image.
Bài viết liên quan
- Cài Docker trên VPS Ubuntu 24.04 chuẩn từng lệnh
- CI/CD tự host trên VPS: nhanh và rẻ hơn
- VPS ổ Enterprise U.2 NVMe RAID 10 tại TND
- Google Workspace cho team dev nhỏ: email tên miền + Gemini + cộng tác
- Giải Pháp Proxy Cao Cấp Cho Người Kinh Doanh Online và Quản Lý Tài Khoản Seller
- Claude Code tutorial: 10 bước từ cài đặt đến dự án đầu
- Playwright + Crawlee + VPS scrape 10k page mỗi ngày không bị ban
- Sentry self-hosted vs Sentry SaaS: dev solo chọn cái nào?
- Stream Netflix US từ Việt Nam 2026: VPN nào hoạt động?



