+31

[Nodejs thực chiến] Dockerize, Containerize nodejs app thật chuẩn

1. Đặt vấn đề:

  • Container là một phương pháp đóng gói ứng dụng để ứng dụng có thể chạy với các phụ thuộc của mình (gồm source code và library, runtime, framework…) một cách độc lập, tách biệt với các chương trình khác.
  • Tool đầu tiên, phổ biến nhất để container ứng dụng là Docker. Ngoài ra còn nhiều nhiều tools khác.
  • Có rất nhiều blog hướng dẫn containerize nodejs nhưng mình thấy rằng khá sơ khai. Với blog này hy vọng giúp mọi ngừoi containerize nodejs app một cách chuẩn hơn.

2. Bước đầu tiên

  • Source code ví dụ mình fork của freecodecamp về repo của mình. source.
  • Để containerize mình tạo file Dockerfile:
#1 Từ base image
FROM node
WORKDIR /app

#2 Coppy toàn bộ source
COPY . .

#3 install dependence
RUN npm i

# build code
RUN npm run build

# run app
CMD [ "npm", "run", "start" ]
  • Run build docker bằng docker-compose: docker compose up --build

  • Check health: curl http://localhost:3333/users Server trả về: {"id":0,"name":"Test containerize"}

  • Check size image: docker image inspect node-containerize_server --format='{{.Size}}', Kết quả: 1755772592 = 1,76G

  • Thời gian build: 70s

  • Các bước tiếp theo mình sẽ lần lượt thực hiện để giảm zize, giảm thời gian build và tăng độ tin cậy.

  • Các image được lưu trử trên các dịch vụ cloud, chi phí tính theo size nên giảm size giúp giảm chi phí

  • Nhiều dịch vụ cicd, như gihub action, circle ci tính tiền theo thừoi gian, giảm thừoi gian buil cũng giám chi phí

3. Tăng độ tin cậy bằng cách tạo dockerignore file

  • File dockerignore sẽ giúp docker
  1. bỏ qua mọt số file không cần thiết cho quá trình build image.
  2. Đảm bảo môi trường container không có liên hệ với các config, các dependence của môi trường local. Ví dụ môi trường local dùng node:14, docker dùng node:16, nếu coppy folder node_modules từ môi trường sẽ có thể dẫn đến bug.
  • touch .dockerignore
# Ignore file IDE setting
.vscode/
# Ignore folder node_modules
node_modules/

# Ignore file build
dist/

# Ignore các file folder khác không cần sửa dung
npm-debug.log
test/
  • Image size không thay đổi nhiều. > 1,7G
  • Thời gian build tăng lên do không còn node_modules nên npm i lâu hơn

4. Giảm size bằng cách chọn base image nhỏ hơn:

  • Ban đầu: FROM node

  • Thành: FROM node:18.13.0-alpine3.16@sha256:3eb81689b639f6a7308c04003653daa94122bfcdbba9945e897b12cfe10bb034 as node

  • Giải thích:

    • node -> imgae có node
    • 18.13.0 version của node, nên chọn các version chẳn 14, 16 18 là các version LTS -> hỗ trợ lâu dài.
    • alpine một hệ điều hành linux nhưng nhẹ hơn ubuntu.
    • Phần @sha256:3....DIGEST Là định danh độc lập và bất biến của image. Có nghĩa là cái iamge này là duy nhất dùng có cập nhật image cùng là alpine cùng version node 18.13.0 nhưng khi build vẫn chỉ nhận image cũ cũng DIGEST.
  • Kết quả khi build image size: 978871571 = 978M = 0.97G Bé hơn nhiều so với 1,72G. Chủ yếu là do alpine có size nhỏ hơn.

  • Thời gian build nhỏ hơn một tý do thừoi gian tải image nhanh hơn.

5. Giảm size bằng cách chỉ install prod dependences.

  • File docker ban đầu:
RUN npm i
RUN npm run build
RUN ["npm", "run", "start"]
  • Update thành:
RUN npm ci
RUN npm run build
RUN npm prune --production
RUN ["node", "dist/main"]
  • Giải thích:
    • npm ci chỉ install từ file package-log.json
    • npm prune --production remove devDependences vốn không cần thiết khi chạy trên production
    • npm run start bản chất là nest start chạy file source chưa build đổi thành node dist/main chạy file code đã build.
    • Mình không dùng npm run start:prod vì sẽ tạo nhiều hơn 1 process, thêm it nhất cái npm . Và việc này làm node app không nhận được tín kiệu kill container. Nếu dùng docker compose setting init: true để chắc chắn nhận tín hiệu kill. test kill command: docker kill --signal=SIGHUP server see more Tiny
  • Kết quả image size: 931057119 = 931M bé hơn một chút so với 978M. Nhưng thực tế khi mình làm việc thì source dự án thường lớn hơn source đang dùng để test nhiều lần nên size sẽ bé hơn khá nhiều.

5. Giảm size bằng multi stage: Buid and Run

  • Thêm vào docker file:
FROM node as server

ENV NODE_ENV=production

WORKDIR /app

# COPY --from=server-builder /app/ /app/
COPY --from=server-builder /app/node_modules /app/node_modules
COPY --from=server-builder /app/.env /app/
COPY --from=server-builder /app/dist /app/dist
  • COPY --from=server-builder /app/.env /app/ Nếu config thông qua configMap hay secretKeys thì không cần coppy file này.
  • ENV NODE_ENV=production có thể tằng hiệu suất cho dự án nodejs: Ví dụ express :
  • Kết quả size giảm còn 414639502 = 414M Chưa bằng 1/2 của 931M, bằng 1/3 1,7G
  • Nguyên nhân do khi install và build tạo rất nhiều file cache, file log. Tạo image mới chỉ coppy những folder cần thiết giúp giảm size file đi rất nhiều.
  • Xem thêm về mutil stage tại document của docker Mutil stage
  • Kết quả file Dockerfile:
#1 Từ base image
FROM node:18.13.0-alpine3.16@sha256:3eb81689b639f6a7308c04003653daa94122bfcdbba9945e897b12cfe10bb034 as node
FROM node as server-builder
WORKDIR /app

#2 Coppy toàn bộ source
COPY . .

#3 install dependence
RUN npm ci

#4 build code
RUN npm run build

#4 remove dev dependence
RUN npm prune --production

#5 Coppy qua image mới
FROM node as server

ENV NODE_ENV=production

WORKDIR /app

COPY --from=server-builder /app/node_modules /app/node_modules
COPY --from=server-builder /app/.env /app/
COPY --from=server-builder /app/dist /app/dist
#6 run app
CMD [ "node", "dist/main.js" ]

6. Giảm thời gian build bằng cách cache dependences

  • Tuy nhiên thời gian build khá chậm: 150s( Sorry mình quên chụp ảnh lại 🤣), chủ yếu là thời gian download dependences. Do depedences ít thay đổi nên ta có thể thêm 2 layer phục vụ viêc cache dependence cho devDependence và prodDependence.
  • File docker sau khi update:
#1 Từ base image
FROM node:18.13.0-alpine3.16@sha256:3eb81689b639f6a7308c04003653daa94122bfcdbba9945e897b12cfe10bb034 as node

#2 Install devDependences
FROM node as server-dev
WORKDIR /app
COPY package.json package-lock.json /app/
COPY prisma /app/prisma
RUN npm ci

#2 Install prodDependences
FROM node as server-prod
WORKDIR /app
COPY package.json package-lock.json /app/
COPY prisma /app/prisma
RUN npm ci --production

#3 Build app
FROM node as server-builder
WORKDIR /app
COPY . .
COPY --from=server-dev /app/node_modules /app/node_modules
RUN npm run build

#4 Run app
FROM node as server
ENV NODE_ENV=production
WORKDIR /app
COPY --from=server-prod /app/node_modules /app/node_modules
COPY --from=server-builder /app/.env /app/
COPY --from=server-builder /app/dist /app/dist
CMD [ "node", "dist/main.js" ]
  • Kết quả khi build lại lần 2 thời gian build giảm đi rất nhiều: còn = 40s

7. Kết luận

  • Cảm ơn các anh nhóm devops việt nam đã chỉ bảo và góp ý.
  • Full source/ branch final here
  • Mọi người có thể xem thêm các bài viết của mình trong seri Node thực chiến
  • Cảm ơn mọi người đã đọc.
  • Mình chia sẽ free, anh em thấy hữu ích động viên mình bằng 1 upvote nhé.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí