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ể.

Tham khảo