Tối ưu Docker Image cho NextJS
Bài đăng này đã không được cập nhật trong 2 năm
Docker image của NextJS
thông thường size sẽ rất nặng. Bài viết này mình sẽ
trình bày 2 cách mình hay sử dụng để tối ưu image
trước khi triển khai lên môi
trường production.
Source code mình để ở đây
Để có thể theo dõi 1 cách trực quan, mình sẽ demo theo 3 kịch bản như sau:
- Sử dụng cách cơ bản để viết
dockerfile
. - Áp dụng
Multi Stage
. - Sử dụng mode
standalone
củaNextJS
kết hợp vớiMulti Stage
.
Lưu ý: Mình sẽ không tập trung nhiều về các cách cơ bản (như hạn chế tạo nhiều Layer, .dockerignore, ...) để tối ưu image, mà sẽ trình bày sâu vào 2 cách chính đã đề cập ở trên. Các bạn có thể theo dõi bài viết Tối ưu Docker image của anh Mai Trung Đức để tham khảo thêm các cách để tối ưu docker image rất hay và chi tiết 😄
Oke !!! Vào thôi
Setup
Đầu tiên, chúng ta cần 1 project sử dụng NextJS
, ở đây để cho nhanh mình sẽ sài
luôn cái blog-starter trong phần examples
của NextJS
. Các bạn có thể clone
ở đây về nha 😄
Cấu trúc của nó trông như này:
.
├── @types
│ └── remark-html.d.ts
├── README.md
├── _posts
│ ├── dynamic-routing.md
│ ├── hello-world.md
│ └── preview.md
├── components
│ ├── alert.tsx
│ ├── avatar.tsx
│ ├── container.tsx
│ ├── cover-image.tsx
│ ├── date-formatter.tsx
│ ├── footer.tsx
│ ├── header.tsx
│ ├── hero-post.tsx
│ ├── intro.tsx
│ ├── layout.tsx
│ ├── markdown-styles.module.css
│ ├── meta.tsx
│ ├── more-stories.tsx
│ ├── post-body.tsx
│ ├── post-header.tsx
│ ├── post-preview.tsx
│ ├── post-title.tsx
│ └── section-separator.tsx
├── interfaces
│ ├── author.ts
│ └── post.ts
├── lib
│ ├── api.ts
│ ├── constants.ts
│ └── markdownToHtml.ts
├── next-env.d.ts
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── index.tsx
│ └── posts
│ └── [slug].tsx
├── postcss.config.js
├── public
│ ├── assets
│ │ └── blog
│ │ ├── authors
│ │ │ ├── jj.jpeg
│ │ │ ├── joe.jpeg
│ │ │ └── tim.jpeg
│ │ ├── dynamic-routing
│ │ │ └── cover.jpg
│ │ ├── hello-world
│ │ │ └── cover.jpg
│ │ └── preview
│ │ └── cover.jpg
│ └── favicon
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ ├── safari-pinned-tab.svg
│ └── site.webmanifest
├── styles
│ └── index.css
├── tailwind.config.js
└── tsconfig.json
Okie, sau đấy cài và chạy thử lên xem phát :v
➜ blog-starter git:(master) ✗ yarn
➜ blog-starter git:(master) ✗ yarn build
➜ blog-starter git:(master) ✗ yarn start
Các bạn truy cập vào localhost:3000 để xem qua nhé :3
Trông cũng ổn đấy :v, ô kê vào phần chính nào ...
Build on Docker
Đầu tiên thì mình sẽ nhét những files
hay folders
không cần thiết vào .dockerignore
:
node_modules
.next
.vscode
*.DS_Store
.gitignore
README.md
.dockerignore
LICENSE
.docker
.gitlab
.git
Như đã đề cập ở trên mình sẽ trình bày 3 kịch bản. Bây giờ, mình sẽ sử dụng cách tạo cơ bản trước theo basic.dockerfile:
FROM node:16-alpine
LABEL author="ductnn <ductn53@gmail.com>"
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add --no-cache git \
&& yarn install --frozen-lockfile \
&& yarn cache clean
COPY . .
RUN yarn build
EXPOSE 3000
CMD ["yarn", "start"]
Tiến hành build
:
➜ blog-starter git:(master) ✗ docker build -t blog-with-basic-dockerfile -f .docker/basic.dockerfile .
# Check docker images
➜ blog-starter git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
blog-with-basic-dockerfile latest b70f75178890 8 seconds ago 370MB
Ở cách build này, image
tạo ra có kích thước là 370MB. Trên thực tế, dự án
của chúng ta sẽ cần nhiều packages
hơn nên với cách build này chúng ta không thể
tối ưu được nhiều.
Tiếp theo, mình sẽ dùng Multi stage
để build dockerfile.
Ở đây, mình chia thành 3 stage
là base
, build
, production
:
# Build BASE
FROM node:16-alpine as BASE
LABEL author="ductnn <ductn53@gmail.com>"
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add --no-cache git \
&& yarn install --frozen-lockfile \
&& yarn cache clean
# Build Image
FROM ductn4/node:16-alpine AS BUILD
LABEL author="ductnn <ductn53@gmail.com>"
WORKDIR /app
COPY --from=BASE /app/node_modules ./node_modules
COPY . .
RUN apk add --no-cache git curl \
&& yarn build \
&& rm -rf node_modules \
&& yarn install --production --frozen-lockfile --ignore-scripts --prefer-offline \
# Follow https://github.com/ductnn/Dockerfile/blob/master/nodejs/node/16/alpine/Dockerfile
&& node-prune
# Build production
FROM node:16-alpine AS PRODUCTION
LABEL author="ductnn <ductn53@gmail.com>"
WORKDIR /app
COPY --from=BUILD /app/package.json /app/yarn.lock ./
COPY --from=BUILD /app/node_modules ./node_modules
COPY --from=BUILD /app/.next ./.next
COPY --from=BUILD /app/public ./public
EXPOSE 3000
CMD ["yarn", "start"]
Ở stage: base
mình tiền hành install package.json
, mục đích là để lấy node_modules
phục vụ cho stage: build
. Trong có trình thực hiện cài đặt mình có dùng thêm
--frozen-lockfile
để "đóng băng" packages
.
Tiếp theo, stage: build
là stage
quan trọng nhất. Bước này, mình sẽ copy
node_modules
ở stage: base
và source code
xong tiến hành yarn build
. Sau
khi build
xong mình sẽ xoá node_modules
đi và ... cài lại 😅 ủa để làm chi ???
Có 1 chút khác mình sử dụng thêm --production
với mục đích chỉ cài các packages
cần thiết trong package.json
và bỏ qua các packages
trong phần devDependencies
.
Điều này sẽ giúp giảm nhẹ hơn node_modules
rất nhiều,
mình không cần lo chạy lỗi vì mình đã build
đầy đủ từ bước trên rồi. Nhưng mình
vẫn muốn thằng node_modules
nhẹ hơn ...
Okies !!! Tiếp đó, mình sử dụng 1 chiếc tool để làm nhẹ đi node_modules
tên
là node-prune, trong base node image
ở stage build
mình đã cài sẵn node-prune
, anh em có thể tham khảo cách cài đặt tại đây.
Cuối cùng, stage: production
mình chỉ cần copy
các files, folders
cần thiết
xuống là được. Build thử nào:
➜ blog-starter git:(master) ✗ docker build -t blog-with-multistage-dockerfile -f .docker/multistage.dockerfile .
# Check docker images
➜ blog-starter git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
blog-with-multistage-dockerfile latest 07c84ea2173a 38 seconds ago 339MB
Tác dụng của node-prune
:
files total 43,924
files removed 12,814
size removed 28 MB
duration 866ms
Vậy là cũng giảm được khá khá so với cách build basic.
Cách cuối cùng, mình vẫn sẽ làm giống cách 2 là sử dụng Multi stage
nhưng sẽ
kết hợp thêm mode: standalone
, cách này thì mình cũng mới sử dụng do các anh
dev maintain
thằng NextJS
cũng đang fix bugs cho phần này.
Để sử dụng mode standalone
mình sẽ tạo thêm file next.config.js
(nếu trong dự
án của anh em có rồi thì thôi :v) và thêm 1 đoạn code nhỏ:
module.exports = {
output: "standalone"
}
Sau đó, chúng ta tiến hành build thử, ở mode
này trong folder .next
sẽ tạo
ra thêm 1 folder con tên là standalone
. Trong đây, NextJS
sẽ tự động copy
các files
và packages
cần thiết để chạy, tham khảo thêm
Oke, bắt đầu viết dockerfile
:
# Build BASE
FROM node:16-alpine as BASE
LABEL author="ductnn"
WORKDIR /app
COPY package.json yarn.lock ./
RUN apk add --no-cache git \
&& yarn --frozen-lockfile \
&& yarn cache clean
# Build Image
FROM ductn4/node:16-alpine AS BUILD
LABEL author="ductnn"
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN apk add --no-cache git curl \
&& yarn build \
&& cd .next/standalone \
# Follow https://github.com/ductnn/Dockerfile/blob/master/nodejs/node/16/alpine/Dockerfile
&& node-prune
# Build production
FROM node:16-alpine AS PRODUCTION
LABEL author="ductnn"
WORKDIR /app
COPY /app/public ./public
COPY /app/next.config.js ./
# Set mode "standalone" in file "next.config.js"
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
Vẫn giống cách làm của cách 2, chỉ khác ở stage: production
ta chỉ cần copy
folders .next/standalone
và .next/static
thay vì copy hết cả folders .next
và node_modules
. Oke build thôi ....
➜ blog-starter git:(master) ✗ docker build -t blog-with-multistage-standalone-dockerfile -f .docker/multistage_standalone.dockerfile .
# Check docker images
➜ blog-starter git:(master) ✗ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
blog-with-multistage-standalone-dockerfile latest 07c84ea2173a 38 seconds ago 119MB
WoW image
build ra chỉ con 119MB nhẹ đáng kể :v
Kết
Vậy là mình vừa trình bày 2 cách để tối ưu image cho dự án sử dụng NextJS
. Anh
em cho mình xin ý kiến và nếu có thêm phương pháp tối ưu nào khác thì share
cho mình với nhé 🥳 🥳 🥳.
Cảm ơn anh em !!!
All rights reserved