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? Tất nhiên, vấn đề mà tồn tại lâu thì chắc chắn sẽ có cách giải quyết. 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ở lê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
Như bạn đã thấy, nó chỉ có hơn 7M. Tuy nhiên, nó sử dụng musl libc
thay vì glibc
, nên cần chú ý khi chuyển từ glibc
sang musl libc
. Bạn có thể tham khảo thêm tại đây.
Nếu bạn vẫn muốn sử dụng các distro khác như Debian
, Ubuntu
, các tag như slim
, buster-slim
, focal
,… cũng là một lựa chọn tốt hơn so với các tag latest
.
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest bf3dc08bfed0 8 days ago 76.2MB
ubuntu focal 2abc4dfd8318 10 days ago 72.8MB
Mặc định, những distro-based này sẽ không có sẵn môi trường chạy Node.js, Python, Java,… và việc cài đặt thêm môi trường, Package Manager,… sẽ có thể khó khăn với người mới cũng như tăng dung lượng của image. Vậy nên Programming Language-based image là một lựa chọn tốt hơn. Ví dụ như node:lts
, python:3.9
, openjdk:11
,…
Sử dụng .dockerignore
.dockerignore
giống như .gitignore
, nó giúp bạn loại bỏ các file không cần thiết khi build image. Điều này giúp giảm dung lượng của image và tăng tốc độ build image.
Ví dụ:
node_modules
.git
.vscode
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 multi-stage build và không sử dụng nó. Ở đây tôi sẽ dockerize một ứng dụng NestJs (Node.js).
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>"
COPY package.json yarn.lock ./
RUN yarn install
COPY . ./
RUN yarn build:prod
FROM node:lts-bookworm-slim AS node_modules
COPY package.json yarn.lock ./
RUN yarn install --prod
FROM node:lts-bookworm-slim
ARG PORT=3000
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY --from=dist dist /usr/src/app/dist
COPY --from=node_modules node_modules /usr/src/app/node_modules
COPY . /usr/src/app
EXPOSE $PORT
CMD [ "yarn", "start:prod" ]
Và đây là kết quả:
docker images
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
Như bạn có thể thấy, cùng sử dụng một base image nhưng image sử dụng multi-stage build nhỏ hơn rất nhiều so với image không sử dụng multi-stage build. Vậy ở đây multi-stage build là gì? Multi-stage build cho phép bạn sử dụng nhiều FROM
trong một Dockerfile. Mỗi FROM
sẽ tạo ra một stage, và bạn có thể copy các file từ stage trước đó sang stage hiện tại. Điều này giúp giảm dung lượng của image và tăng tốc độ build image.
Như ở ví dụ trên, không cần thiết phải cài đặt các package dev khi chạy ứng dụng mà chỉ cần khi build, chúng ta sẽ cài đặt các package dev trong stage đầu tiên, buid và copy các file cần thiết sang stage cuối cùng. Vậy tại sao lại có node_modules
stage mà không cài đặt trực tiếp trong stage cuối cùng? Vì khi chia thành 2 stage như vậy, với Docker Buildkit
chúng sẽ chạy song song, giúp tăng tốc độ build image.
=> [node_modules 3/3] RUN yarn install --prod
=> => # yarn install v1.22.19
=> => # [1/4] Resolving packages...
=> => # [2/4] Fetching packages...
=> [dist 3/5] RUN yarn install
=> => # yarn install v1.22.19
=> => # [1/4] Resolving packages...
=> => # [2/4] Fetching packages...
Sử dụng số lượng layer ít nhất có thể
Khi build image, mỗi lệnh như FROM
, COPY
, ADD
sẽ tạo ra một layer. Mỗi layer được tạo ra sẽ tăng dung lượng và tăng thời gian build image. Vì vậy, hãy cố gắng kết hợp các lệnh thành một lệnh để giảm số lượng layer.
Ở đây tôi có 2 Dockerfile ứng với từng cách sử dụng câu lệnh.
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
[+] Building 26.8s (9/9) FINISHED
REPOSITORY TAG IMAGE ID CREATED SIZE
multiple-layer latest 2373a9518de4 41 seconds ago 239MB
FROM ubuntu:focal
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install --no-install-recommends -y vim dnsutils
[+] Building 25.2s (6/6) FINISHED
REPOSITORY TAG IMAGE ID CREATED SIZE
single-layer latest 2373a9518de4 41 seconds ago 231MB
Bằng cách này, bạn có thể thấy thời gian để build giảm từ 26.8s xuống còn 25.2s, và dung lượng giảm từ 239MB xuống 231MB. Trông có vẻ không nhiều lắm nhưng nếu Dockerfile của bạn cần cài đặt nhiều package hơn, sự khác biệt sẽ rất lớn.
Tuy nhiên, cái gì mà “quá” cũng sẽ không tốt đúng không? Việc gom các câu lệnh vào thành một có thể sẽ ảnh hưởng khá lớn tới thời gian build nếu bạn không biết sử dụng một cách đúng đắn. Để hiểu rõ hơn về vấn đề này thì chúng ta sẽ chuyển sang phần tiếp theo.
Caching trong Docker
Như tôi đã nói ở phần trước, mỗi lệnh như FROM
, COPY
, ADD
sẽ tạo ra một layer. Và những layer này sẽ được cache để sử dụng cho những lần build sau đó nếu layer đó không bị thay đổi. Vậy khi một layer bị thay đổi thì điều gì sẽ xảy ra? Tất nhiên cache sẽ trở lên vô dụng, và chúng ta sẽ phải build lại layer hiện tại và tất cả layer sau đó nữa.
Ví dụ:
# Good practice
FROM node:lts AS build
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
# Bad practice
FROM node:lts AS build
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
Ở ví dụ trên, nếu chúng ta thay đổi một file nào đó trong thư mục .
thì layer COPY . .
sẽ bị thay đổi, và tất cả layer sau đó cũng sẽ bị thay đổi. Vậy nên, hãy cố gắng đặt những lệnh mà thay đổi nhiều nhất vào cuối cùng.
Đến đây, bạn cũng đã có thể hiểu được tại sao ở phần trên tôi nói “cái gì mà quá cũng không tốt” rồi đúng không? Hãy cân nhắc kỹ trước khi gom các lệnh lại với nhau để tận dụng tốt cơ chế caching của Docker.
Một số tools hỗ trợ tối ưu hóa Dockerfile
Tổng kết
Như vậy, qua bài viết này, tôi đã giới thiệu cho bạn 5 cách để giúp tối ưu hóa Dockerfile của mình. Hy vọng rằng nó có thể giúp đỡ cho bạn trong việc xây dựng Docker Image của riêng mình. Nếu bạn có bất kỳ ý kiến hoặc góp ý nào, hãy để lại bình luận bên dưới. Chúc bạn thành công!