Vấn đề
Docker là một công cụ phi thường mạnh mẽ, giúp chúng ta tạo ra các container chứa ứng dụng của mình. Tuy nhiên, một vấn đề đã tồn tại từ lâu là việc xây dựng một image tốn rất nhiều thời gian và dung lượng của image thường cao đến vài GB.
Vậy làm sao để giảm dung lượng của image và tăng tốc độ xây dựng image? Trong bài viết này, tôi sẽ giới thiệu một số cách để giúp bạn tối ưu hóa Dockerfile của mình.
Làm sao để Image trở nên “ngon” hơn
Sử dụng lightweight base image
Một trong những cách đơn giản nhất để giảm dung lượng của image là sử dụng lightweight base image. Ví dụ điển hình là Alpine Linux.
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 05455a08881e 3 months ago 7.38MB
Chỉ hơn 7MB — nhỏ hơn nhiều so với hầu hết các base image khác. Tuy nhiên, Alpine sử dụng musl libc thay vì glibc, nên một số thư viện native có thể hoạt động không như kỳ vọng khi chuyển sang. Đây là điều cần kiểm tra kỹ trước khi đưa vào production.
Nếu vẫn muốn dùng các distro quen thuộc như Debian hay Ubuntu, các tag như slim, buster-slim, focal… là lựa chọn tốt hơn so với latest.
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest bf3dc08bfed0 8 days ago 76.2MB
ubuntu focal 2abc4dfd8318 10 days ago 72.8MB
Tuy nhiên, distro-based image mặc định không có sẵn môi trường chạy Node.js, Python, Java,… Việc cài thêm sẽ phức tạp hơn và làm tăng dung lượng. Vì vậy Programming Language-based image thường là lựa chọn thực tế hơn — ví dụ node:lts-slim, python:3.11-slim, eclipse-temurin:21-jre-alpine,…
Nếu muốn đi xa hơn, Distroless image của Google là lựa chọn đáng cân nhắc — thậm chí còn nhỏ và bảo mật hơn cả Alpine vì không chứa shell, package manager hay bất kỳ công cụ hệ thống nào không cần thiết. Phù hợp cho production khi bạn đã biết rõ runtime cần gì.
Sử dụng .dockerignore
.dockerignore giống như .gitignore, giúp loại bỏ các file không cần thiết khi build image — giảm dung lượng build context gửi lên Docker daemon và tăng tốc độ build.
node_modules
.git
.vscode
*.log
Sử dụng multi-stage build
Trước tiên, tôi sẽ cho các bạn thấy sự khác biệt khi sử dụng và không sử dụng multi-stage build. Ở đây tôi sẽ dockerize một ứng dụng NestJS.
Dockerfile không sử dụng multi-stage build:
FROM node:lts-bookworm-slim
LABEL author="Thanh Pham <thanhtpham99@gmail.com>"
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build:prod
EXPOSE 3000
CMD [ "yarn", "start:prod" ]
Dockerfile sử dụng multi-stage build:
FROM node:lts-bookworm-slim AS dist
LABEL author="Thanh Pham <thanhtpham99@gmail.com>"
WORKDIR /build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . ./
RUN yarn build:prod
FROM node:lts-bookworm-slim AS node_modules
WORKDIR /deps
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --prod && yarn cache clean
FROM node:lts-bookworm-slim
ARG PORT=3000
WORKDIR /usr/src/app
# Chỉ copy đúng những gì cần để chạy
COPY --from=dist /build/dist ./dist
COPY --from=node_modules /deps/node_modules ./node_modules
COPY package.json ./
# Không chạy với quyền root
USER node
EXPOSE $PORT
CMD [ "yarn", "start:prod" ]
Kết quả:
REPOSITORY TAG IMAGE ID CREATED SIZE
my-node-app-multiple-stage latest 85a32e4d3228 21 seconds ago 342MB
my-node-app latest 44dbc7d5a222 21 minutes ago 1.02GB
Multi-stage build cho phép dùng nhiều FROM trong một Dockerfile. Mỗi FROM tạo ra một stage độc lập — stage cuối chỉ copy đúng những gì cần để chạy, không kéo theo dev dependencies hay source code gốc.
--frozen-lockfile đảm bảo yarn không tự cập nhật lockfile, giữ môi trường nhất quán giữa các lần build. USER node giúp container không chạy với quyền root — một Docker image “ngon” không chỉ nhỏ mà còn phải bảo mật.
Sử dụng số lượng layer ít nhất có thể
Mỗi lệnh RUN, COPY, ADD tạo ra một layer. Mỗi layer làm tăng dung lượng và thời gian build. Hãy gộp các lệnh liên quan thành một:
# Không tối ưu
FROM ubuntu:focal
RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get install -y vim
RUN apt-get install dnsutils -y
# Tối ưu hơn
FROM ubuntu:focal
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install --no-install-recommends -y vim dnsutils && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* xóa cache của apt ngay trong cùng layer — nếu để riêng một RUN khác, cache đó vẫn tồn tại trong layer trước và không giảm được dung lượng thực sự.
REPOSITORY TAG SIZE
multiple-layer latest 239MB
single-layer latest 231MB
Với ví dụ đơn giản thì chênh lệch không nhiều, nhưng khi Dockerfile cài đặt nhiều package hơn, sự khác biệt sẽ lớn hơn đáng kể.
Caching trong Docker
Mỗi layer sau khi build xong sẽ được cache lại. Nếu layer không thay đổi ở lần build tiếp theo, Docker dùng lại cache thay vì build từ đầu. Tuy nhiên, khi một layer thay đổi, tất cả layer phía sau cũng bị invalid và phải build lại.
Nguyên tắc: layer ít thay đổi đặt lên trên, layer hay thay đổi đặt xuống dưới.
# Tốt — package.json thay đổi ít hơn source code
FROM node:lts AS build
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install # Cache layer này nếu package.json không đổi
COPY . . # Layer này thay đổi thường xuyên hơn
RUN yarn build
# Không tốt — thay đổi bất kỳ file nào cũng làm yarn install chạy lại
FROM node:lts AS build
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
Ngoài ra, bạn có thể kích hoạt BuildKit để build nhanh hơn — hỗ trợ build song song các stage độc lập và quản lý cache thông minh hơn:
DOCKER_BUILDKIT=1 docker build -t my-app .
Từ Docker 23.0 trở đi, BuildKit đã được bật mặc định.
Một số tools hỗ trợ
- Dive — inspect từng layer, xem cụ thể layer nào đang chiếm dung lượng nhiều nhất
- SlimToolKit — tự động phân tích và shrink image
- Docker Scout — scan vulnerability trong image
Tổng kết
Tối ưu Docker Image không chỉ là giảm dung lượng — mà còn là tăng tốc độ build, đảm bảo tính nhất quán và bảo mật. Năm kỹ thuật trong bài có thể áp dụng độc lập, nhưng kết hợp lại — lightweight base image, .dockerignore, multi-stage build, giảm số layer và tận dụng cache — sẽ cho ra image nhỏ, nhanh và an toàn hơn đáng kể.

