+16

Cách Mình Giảm Thời Gian Build Docker Xuống 200 Lần!!

Từ một Docker build kéo dài 185,7 giây với image nặng 3,61GB, mình đã tối ưu xuống còn 0,9 giây995MB nhờ viết lại Dockerfile theo kiểu multi-stage. Trong bài viết này, mình sẽ chia sẻ cách mình làm điều đó, bao gồm việc phân tích Dockerfile ban đầu, những sai lầm nghiêm trọng khiến thời gian build chậm chạp và image "phình to", cũng như giải pháp tối ưu với Docker multi-stage build. Cuối cùng, chúng ta sẽ xem kết quả so sánh trước và sau khi tối ưu thông qua biểu đồ minh họa. Hãy bắt đầu thôi!

Note: Đây là bản tiếng Việt từ bài viết gốc của mình ở Medium, nếu thấy hay thì hãy cho mình 1 claps ở đây nhé, mãi iu : https://medium.com/@nhattrile11/how-i-reduced-docker-build-time-by-99-51-and-image-size-by-73-08-99f5c8337377

Dockerfile ban đầu: Chậm chạp và cồng kềnh

Trước khi tối ưu, Dockerfile của mình trông như thế này:

# Dockerfile ban đầu (có vấn đề)
FROM node:16

WORKDIR /app

# Sao chép toàn bộ source code vào image
COPY . .

# Cài đặt dependencies bên trong container
RUN npm install

# Build ứng dụng 
RUN npm run build

# Khởi động ứng dụng
CMD ["npm", "start"]

Có gì sai ở đây?

Ở cái nhìn đầu tiên, Dockerfile này có vẻ hoạt động được: nó lấy một image Node.js, copy mã nguồn, cài dependency, build và chạy. Thực tế, mình cũng đã dùng Dockerfile này một thời gian, cho đến khi dự án lớn dần lên. Rồi bùm! Thời gian build tăng chóng mặt, dung lượng image cũng vậy. 😵

Hãy mổ xẻ những vấn đề tiềm ẩn trong Dockerfile ban đầu này và tại sao nó khiến build time tới ~3 phút (185 giây) và image size hơn 3,6GB.


Những sai lầm nghiêm trọng trong Dockerfile ban đầu

Dưới đây là các sai lầm chính mà mình đã mắc phải (rất may, đây cũng là những lỗi khá phổ biến, nên bạn không đơn độc đâu 😅):

1. Copy toàn bộ project mà không .dockerignore

Lệnh COPY . . sẽ chép toàn bộ thư mục project vào image. Nếu bạn không loại trừ node_modules, .next/cache hay các thư mục build cache khác, Docker sẽ đem tất cả những đống này vào image. Kết quả là image phình to một cách không cần thiết.

Trong trường hợp của mình, việc copy cả node_modules.next/cache khiến context build lên đến hàng GB, mỗi lần build Docker phải chuyển một đống dữ liệu thừa thãi này vào image. Điều này làm chậm quá trình build và tạo ra một image cục mịch.

2. Cài đặt dependencies trực tiếp trong container (không tối ưu cache)

Docker build tạo ra các lớp (layer) cache cho mỗi lệnh. Dockerfile ban đầu cài dependencies bằng lệnh RUN npm install sau khi copy toàn bộ mã nguồn. Nghĩa là mỗi khi bất kỳ file nào trong project thay đổi (ví dụ chỉnh sửa 1 dòng code), layer npm install bị mất cache và phải chạy lại từ đầu. 😫 Điều này cực kỳ lãng phí thời gian.

Lẽ ra, mình nên tách bước cài dependencies ra và chỉ chạy lại khi file khai báo dependency (package.json/package-lock.json) thay đổi. Việc không tận dụng cache hợp lý khiến mỗi lần build giống như một cực hình, cài đi cài lại hàng trăm package dù phần lớn không đổi.

3. Không dùng multi-stage build

Dockerfile một stage duy nhất nghĩa là image cuối cùng chứa luôn cả môi trường build. Tất cả dependencies (kể cả devDependencies), code nguồn, thậm chí tool build đều nằm trong image production.

Trong trường hợp Node.js (ở case này là Next.js), điều này đồng nghĩa với việc image bao gồm cả những thứ chỉ cần cho quá trình build (Webpack, Babel, v.v.) và các file nguồn không còn cần thiết sau khi đã build xong. Hậu quả là dung lượng image tăng vọt. Ngoài ra, image cồng kềnh còn làm chậm việc push/pull image lên registry và deploy.


Giải pháp tối ưu: Viết lại Dockerfile đa tầng (multi-stage)

Sau khi nhận ra các vấn đề trên, mình quyết định viết lại Dockerfile theo hướng multi-stage build. Mục tiêu là: chỉ giữ lại những gì cần thiết cho sản phẩm cuối, và tận dụng tối đa cache để tăng tốc độ build.

Dưới đây là Dockerfile mới sau khi tối ưu:

# Giai đoạn cài đặt dependencies (deps stage)
FROM --platform=linux/amd64 node:19-bullseye-slim AS deps

WORKDIR /app

# Sao chép các file package
COPY package.json package-lock.json* ./

# Cài đủ prod + dev dependencies 
RUN npm ci

# Giai đoạn build ứng dụng (builder stage)
FROM --platform=linux/amd64 node:19-bullseye-slim AS builder

WORKDIR /app

# Sao chép thư viện đã cài từ giai đoạn deps
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build ứng dụng, loại bỏ cache và dev dependencies sau khi build
RUN npm run build \
    && npm prune --production \
    && rm -rf .next/cache

# Giai đoạn chạy ứng dụng (runner stage)
FROM --platform=linux/amd64 node:19-alpine AS runner

WORKDIR /app

# Sao chép các file cần thiết từ giai đoạn builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/package.json ./

# Mở cổng 3000
EXPOSE 3000

# Thiết lập môi trường production
ENV NODE_ENV=production

# Lệnh mặc định để chạy ứng dụng
CMD ["npm", "start"]

.dockerignore

Dockerfile
.dockerignore
node_modules
npm - debug.log
README.md
.next
.git

Giải thích các thay đổi

Hãy cùng mình điểm lại nhanh xem từng thay đổi thế nào nha:


  • Chép có chọn lọc & dùng .dockerignore: Nhờ loại bỏ mấy thư mục thừa thãi bằng .dockerignore và chỉ copy đúng những file cần thiết, quá trình build Docker giờ nhẹ tênh :3

  • Tách riêng bước cài dependencies: Việc cài dependency giờ đã cache-friendly hơn rất nhiều. Miễn là package.jsonpackage-lock.json không đổi, Docker sẽ dùng lại cache và khỏi phải chạy npm ci lại từ đầu, tiết kiệm khối thời gian chờ đợi.

  • Tận dụng multi-stage build: Vì đã tách rõ các giai đoạn – từ cài deps (deps), build app (builder), tới chạy production (runner) – nên đã giúp mình giữ lại đúng những gì cần thiết cho image cuối cùng.
    DevDependencies, tool build và mớ file build tạm đều bay màu, image gọn gàng hẳn, đẩy lên registry cũng nhanh hơn rõ.


Tại sao mình dùng npm ci thay vì npm install?

Mình đã thay npm install bằng npm cinpm ci mang lại một vài lợi ích cực kỳ quan trọng sau:

  • Ổn định:
    npm ci tuân thủ nghiêm ngặt các phiên bản đã được khóa trong package-lock.json, đảm bảo môi trường build luôn đồng nhất và có thể tái tạo chính xác trên mọi máy (kể cả khi nếu đã có node_modules thì nó cũng sẽ xóa luôn và tạo lại mới hoàn toàn).

  • Nhanh hơn:
    bỏ qua một số bước kiểm tra không cần thiết (như kiểm tra tương thích hay tạo mới file lock, bỏ qua package.json mà cài thẳng từ package-lock.json) nên tốc độ cài đặt nhanh hơn đáng kể.

  • Đáng tin cậy:
    Cực kỳ phù hợp cho các quy trình CI/CD – nơi sự nhất quán giữa các lần build là điều bắt buộc.


Kết quả tối ưu hóa: Nhanh hơn, nhẹ hơn đáng kể

Sau khi áp dụng Dockerfile multi-stage mới, kết quả thu được thực sự ấn tượng:

  • Thời gian build giảm từ 185,7 giây xuống chỉ còn 0,9 giây (nhanh hơn gấp ~200 lần!). Việc build Docker image bây giờ gần như "một cái chớp mắt", nhấn build xong ngay, không kịp làm ngụm cà phê nào ☕ nữa.
  • Dung lượng image giảm từ 3,61GB xuống 995MB. Image nhỏ hơn ~1/4 so với trước. Giờ đây mình có thể lưu trữ 3–4 phiên bản image mới bằng dung lượng của đúng 1 bản cũ trước kia.

Để trực quan hơn, hãy xem biểu đồ so sánh dưới đây giữa trước và sau tối ưu:

image.png

Màu đỏ ("Trước") cho thấy Docker build ban đầu. Màu xanh lá ("Sau")

Nhìn vào biểu đồ, sự khác biệt thật sự rõ rệt. Thanh màu xanh lá thấp lè tè gần sát trục 0 cho thời gian build “Sau” chứng tỏ build mới nhanh kinh khủng khiếp so với thanh đỏ cao ngất trời “Trước”. Tương tự, dung lượng image đã co lại đáng kể (từ 3610MB còn 995MB). Những con số không biết nói dối – chúng chứng minh việc đầu tư viết lại Dockerfile đã đem lại hiệu quả vượt bậc.


Lời kết

Qua câu chuyện tối ưu Dockerfile của chính mình, bài học rút ra là: đừng coi thường những dòng lệnh trong Dockerfile. Chỉ vài sai lầm nhỏ (copy thừa vài thư mục, cài package không đúng chỗ, quên multi-stage) có thể tích tụ thành vấn đề lớn làm chậm cả quy trình CI/CD và tạo ra những Docker image cồng kềnh.

Hy vọng những kinh nghiệm và mẹo tối ưu trên đây sẽ hữu ích cho bạn trong việc viết Dockerfile. Hãy thử áp dụng và biết đâu bạn cũng sẽ ngạc nhiên với kết quả đạt được. 🚀


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí