Làm thế nào để giảm kích thước Docker Image?
Khi ta tạo một container image cho một ứng dụng điển hình, nó sẽ chứa một base image, các dependencies/tệp tin/cấu hình và “cruft” (phần mềm không cần thiết).

Vì vậy, việc tối ưu hóa Docker Image phụ thuộc vào cách ta quản lý các tài nguyên này bên trong image hiệu quả như thế nào.
Hãy cùng xem qua các phương pháp tối ưu hóa Docker image đã được thiết lập. Ngoài ra, chúng tôi cung cấp các ví dụ thực tiễn để hiểu rõ việc tối ưu hóa Docker image trong thực tế.
Bạn có thể sử dụng các ví dụ được đề cập trong bài viết hoặc thử áp dụng các kỹ thuật tối ưu hóa vào Dockerfile hiện có của mình.
Dưới đây là các phương pháp mà chúng ta có thể đạt được việc tối ưu hóa Docker image:
- Sử dụng distroless/minimal base images
- Xây dựng nhiều giai đoạn (multistage builds)
- Giảm thiểu số lớp (layers)
- Hiểu rõ cơ chế caching
- Sử dụng Dockerignore
- Lưu dữ liệu ứng dụng ở nơi khác
Phương pháp 1: Sử dụng Minimal Base Images
Tập trung đầu tiên của bạn nên là chọn đúng base image với footprint hệ điều hành nhỏ nhất có thể.
Một ví dụ là Alpine base image. Image Alpine có thể nhỏ chỉ 5.59MB. Nó không chỉ nhỏ mà còn rất an toàn.
alpine latest c059bfaa849c 5.59MB
Bạn có thể giảm thêm kích thước base image bằng cách sử dụng distroless images, phiên bản hệ điều hành được tối giản. Distroless base images có sẵn cho Java, Node.js, Python, Rust, v.v.
Lưu ý: Bạn không thể sử dụng trực tiếp các base image công khai trong môi trường dự án. Bạn cần sự phê duyệt từ đội ngũ bảo mật của tổ chức để sử dụng base image này.
Phương pháp 2: Sử dụng Multistage Build trong Docker
Mô hình multistage build (xây dựng nhiều giai đoạn) được phát triển từ khái niệm mô hình builder, trong đó chúng ta sử dụng các Dockerfile khác nhau để xây dựng và đóng gói mã ứng dụng. Mặc dù mô hình này giúp giảm kích thước của image Docker, nhưng nó có thể làm tăng khối lượng công việc cho các pipeline xây dựng.
Trong multistage build, chúng ta sử dụng các image trung gian (build stages) để biên dịch mã, cài đặt các dependencies (thư viện phụ thuộc), và đóng gói tệp tin. Ý tưởng chính là loại bỏ các lớp không cần thiết trong image Docker. Sau đó, chỉ các tệp tin ứng dụng cần thiết để chạy ứng dụng mới được sao chép vào một image nhẹ hơn, chỉ chứa các thư viện cần thiết.
Hãy cùng xem qua ví dụ thực tế để hiểu rõ cách tối ưu hóa Dockerfile cho một ứng dụng Node.js đơn giản.
Cấu trúc thư mục ứng dụng:
├── Dockerfile1
├── Dockerfile2
├── env
├── index.js
└── package.json
Dockerfile1:
FROM node:16
COPY . .
RUN npm install
EXPOSE 3000
CMD [ "node", "index.js" ]
Kích thước của image này là 910MB.
Giờ hãy sử dụng phương pháp này để tạo một multistage build.
Chúng ta sẽ sử dụng node:16
làm image để cài đặt tất cả các thư viện phụ thuộc, sau đó chuyển nội dung sang một image nhẹ hơn dựa trên alpine
. image alpine
có các tiện ích tối thiểu, do đó rất nhẹ.
Bạn cũng có thể có nhiều giai đoạn khác nhau trong một Dockerfile với các image khác nhau. Ví dụ, bạn có thể có các giai đoạn cho việc xây dựng, kiểm thử, phân tích tĩnh, và đóng gói với các image khác nhau.
Dockerfile2 (Multistage Build):
FROM node:18 as build
WORKDIR /app
COPY package.json index.js env ./
RUN npm install
FROM node:alpine as main
COPY --from=build /app /
EXPOSE 8080
CMD ["index.js"]
Kích thước của image mới đã giảm xuống chỉ còn 171MB so với image ban đầu chứa tất cả các thư viện phụ thuộc.
Đây là một sự tối ưu hóa hơn 80%!
Tuy nhiên, nếu chúng ta sử dụng cùng một image cho cả giai đoạn xây dựng và chạy ứng dụng, kích thước không giảm nhiều. Bạn có thể giảm kích thước thêm nữa bằng cách sử dụng image distroless. Dưới đây là Dockerfile với bước multistage build sử dụng image Google Node.js distroless thay vì alpine
.
Dockerfile2 (Distroless):
FROM node:18 as build
WORKDIR /app
COPY package.json index.js env ./
RUN npm install
FROM gcr.io/distroless/nodejs
COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]
Multistage build giúp giảm kích thước image Docker đáng kể, tối ưu hóa tài nguyên hệ thống khi triển khai ứng dụng.
Phương pháp 3: Giảm Thiểu Số Lượng Lớp (Layers)
Docker images hoạt động theo cách sau: mỗi chỉ thị RUN
, COPY
, FROM
trong Dockerfile sẽ thêm một lớp mới, và mỗi lớp sẽ làm tăng thời gian thực thi quá trình xây dựng và tăng yêu cầu lưu trữ của image.
Hãy cùng xem điều này trong thực tế với một ví dụ cụ thể: chúng ta sẽ tạo một Ubuntu image với các thư viện đã được cập nhật và nâng cấp, cùng với một số gói cần thiết như vim, net-tools, dnsutils.
Dockerfile
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install vim -y
RUN apt-get install net-tools -y
RUN apt-get install dnsutils -y
Kích thước image là 227MB.
Bây giờ, hãy kết hợp các lệnh RUN
thành một lớp duy nhất và lưu lại thành Dockerfile.
Dockerfile
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install --no-install-recommends vim net-tools dnsutils -y
Vậy kích thước image giờ đây là 216MB. Bằng cách sử dụng kỹ thuật tối ưu hóa này, thời gian thực thi đã giảm từ 117.1 giây xuống 91.7 giây và kích thước lưu trữ giảm từ 227MB xuống 216MB.
Phương pháp 4: Hiểu Về Bộ Nhớ Cache (Caching)
Thường thì, cùng một image cần phải được xây dựng lại nhiều lần với những thay đổi nhỏ trong mã nguồn.
Vì Docker sử dụng hệ thống tệp dạng lớp (layered filesystem), mỗi chỉ thị sẽ tạo ra một lớp. Do đó, Docker sẽ lưu vào bộ nhớ cache lớp này và có thể sử dụng lại nó nếu nó không bị thay đổi.
Với khái niệm này, được khuyến nghị là nên thêm các dòng lệnh dùng để cài đặt các phụ thuộc và gói trước các lệnh COPY
trong Dockerfile.
Lý do cho điều này là Docker có thể lưu vào bộ nhớ cache image với các phụ thuộc cần thiết, và bộ nhớ cache này có thể được sử dụng trong các lần xây dựng tiếp theo khi mã nguồn bị thay đổi.
Ngoài ra, các lệnh COPY
và ADD
trong Dockerfile sẽ làm vô hiệu hóa bộ nhớ cache cho các lớp tiếp theo. Điều này có nghĩa là Docker sẽ xây dựng lại tất cả các lớp sau lệnh COPY
và ADD
.
Điều này có nghĩa là, được khuyến nghị là thêm các lệnh ít có khả năng thay đổi hơn ở đầu Dockerfile.
Ví dụ, hãy cùng xem xét hai Dockerfile sau đây.
Dockerfile (Ví dụ Tốt)
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
COPY . .
Dockerfile (Ví dụ Kém Tối Ưu)
FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
COPY . .
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y vim net-tools dnsutils
Ví dụ mình có dùng API .NET8
Dockerfile (Ví dụ Kém Tối Ưu)
FROM mcr.microsoft.com/dotnet/sdk:6.0
WORKDIR /app
COPY ./publish .
ENTRYPOINT ["dotnet", "NVC.Test.API.Services.dll"]
Dockerfile (Ví dụ Tốt)
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
FROM base AS app
WORKDIR /app
COPY ./publish .
ENTRYPOINT ["dotnet", "NVC.Test.API.Services.dll"]

Áp dụng lên dự án cá nhân mình đã giảm được từ 869MB xuống 337MB
Phương pháp 5: Sử Dụng .dockerignore
Theo quy tắc, chỉ những tệp cần thiết mới cần được sao chép vào image Docker.
Docker có thể bỏ qua các tệp có trong thư mục làm việc nếu được cấu hình trong tệp .dockerignore
.
Việc này cũng cải thiện bộ nhớ cache bằng cách bỏ qua các tệp không cần thiết và ngăn chặn việc làm vô hiệu hóa bộ nhớ cache không cần thiết.
Tính năng này nên được ghi nhớ khi tối ưu hóa image Docker.
.dockerignore
.git*
*.sln*
*.proj*
obj/
bin/
Phương pháp 6: Giữ Dữ Liệu Ứng Dụng Ở Nơi Khác
Lưu trữ dữ liệu ứng dụng trong image sẽ làm tăng kích thước của image một cách không cần thiết.
Rất được khuyến nghị là sử dụng tính năng volume của các runtime container để giữ cho image tách biệt với dữ liệu.
Nếu bạn đang sử dụng Kubernetes, hãy đảm bảo rằng dữ liệu được lưu trữ ở nơi khác để tránh làm tăng kích thước của image Docker.