Hello các bạn lại là mình đây…
Lâu lắm rồi mới lại được ngồi viết blog, hơn nửa năm rồi . Có ai nhớ mình ko, các bạn hãy nói là có đi cho mình đỡ cảm thấy quê
Thời gian vừa rồi mặc không được viết blog để chia sẻ với các bạn nhiều nhưng bù lại thì trong quá trình làm việc và giúp đỡ nhiều bạn chập chững làm quen với Docker mình nhận thấy có nhiều vấn đề các bạn chưa thực sự hiểu để có thể áp dụng vào project riêng hoặc cho công việc của từng người.
Ở bài này chúng ta sẽ cùng nhau tìm hiểu các để viết build được một Docker image tối ưu, production-ready cùng với đó là tăng tốc độ build image ở các môi trường khác nhau (local và CI) nhé.
Bài này bạn có thể xem nó như là 1 bài chi tiết và cụ thể thực tế hơn của bài Tối ưu Docker image
Setup
Đầu tiên các bạn clone source code của mình ở đây. Ở bài này chúng ta chỉ quan tâm tới folder docker-optimize-image thôi nhé.
Tổng quan, bài này mình có setup sẵn cho các bạn 1 project lấy từ source code của NextJS Ecommerce. Mình muốn tìm 1 project nào đó nặng nặng chút giống giống thực tế chút mà tìm mãi ko thấy cái nào thực sự ưng ý (project thật thì ko share lên đây đc rồi ), nên chọn project này vậy.
Sau khi clone về các bạn chạy yarn install
sau đó chạy yarn dev
để start project mình kiểm tra trước là mọi thứ vẫn chạy ổn định nhé. Khi mở trình duyệt các bạn sẽ thấy nó show như sau:
Web “nhà người ta” làm nom chất lượng nhỉ, nổi bật, hiện đại, mượt, màu sắc đẹp. Ôi mà thôi mình lại chuẩn bị lan man đấy
Dockerize project
Đầu tiên chúng ta sẽ cùng nhau “dockerize” project này nhé.
Cho bạn nào chưa biết thì “dockerize” (động từ) ý chỉ việc bạn đóng gói (package), deploy và chạy app trong môi trường container
Đầu tiên các bạn tạo cho mình Dockerfile
ở trong folder hiện tại (docker-optimize-image
) nhé:
FROM node:14-alpine# Đặt đường dẫn trong container nơi ta sẽ đưa code vàoWORKDIR /app# Copy toàn bộ code ở thư mục hiện tại trên môi trường gốc -> vào đường dẫn hiện tại trong container (/app)COPY . .# Cài dependencies cho project -> build project -> start projectRUN yarn installRUN yarn buildCMD ["yarn", "start"]
Trông có vẻ tương đối đơn giản và dễ hiểu phải ko các bạn, toàn kiến thức cũ ko à
Tiếp theo trước khi build các bạn thêm cho mình file .dockerignore
nhé (ta ko muốn copy cả folder node_modules
to tổ bố từ môi trường ngoài lúc build image đâu phải ko ):
node_modules
# .next là folder build do Next tạo ra
# (nếu trước đó bạn đã chạy thử ở môi trường ngoài thì sẽ thấy)
.next
.vscode
Âu cây nom có vẻ ổn rồi đó tiến hành build image thôi nào:
docker build -t test-nextjs .
Trong thời gian ngồi chờ hít đất đôi chục cái lấy sức khoẻ vượt qua dịch bệnh nàooooo 💪 💪💪
Sau vài phút quay trở lại kiểm tra terminal để đảm bảo mọi thứ đã thành công hay chưa nhé:
Ta chạy thử image mới build lên thôi nào:
docker run -it -p 3000:3000 test-nextjs
ở bên trên khi chạy mình thêm option
-it
mục đích là muốn chạy container như 1 interactive process (1 tiến trình có thể tương tác được) để lát nữa ta có thể stop container bằng cách bấm CTRL-C (hoặc CTRL-Z, CTRL-D), không thì lát nữa container nó sẽ “ko chịu” tắt kể cả khi ta tắt terminal
Sau đó ta mở trình duyệt ở http://localhost:3000
sẽ thấy điều tương tự ta đã làm ở phần setup đầu tiên trong bài này nhé
Bắt đầu vào chủ đề chính của bài hôm nay thôi nào
Tăng tốc độ build image
Tận dụng docker layer caching
Đầu tiên các bạn thêm vào cuối file pages/_app.tsx
1 dòng console.log
bất kì như sau:
console.log('Hello world')
Sau đó ta tiến hành build lại image:
docker build -t test-nextjs .
Quan sát ở cửa sổ Terminal ta sẽ thấy như sau:
Docker đã vừa thực hiện lại toàn bộ các bước ta định nghĩa trong Dockerfile: copy, install, build, run… mất vài phút mới chạy xong được
Ta lại tiếp tục thêm vào file pages/_app.tsx
1 dòng console.log nữa:
console.log('Hello world')
console.log('Nice to meet you')
Sau đó ta lại tiếp tục build image, và lại để ý terminal thấy rằng ta vẫn phải chờ mất phút lận để Docker làm đi làm lại những công việc ta khai báo ở Dockerfile.
Điều này hết sức mất thời gian đặc biệt là trong các dự án thực tế khi codebase lớn, dependencies nhiều, cài lâu, build lâu có khi tới cả 20-30′ , 1 tiếng mới xong. Làm tốn resource, tăng thời gian chờ, đặc biệt nếu ta đang muốn test cái gì đó ở local chẳng hạn, cứ thêm 1 dòng console.log
lại chờ vài phút
Và để khắc phục điều này thì Docker mang lại cho chúng ta 1 tính năng cực hữu ích gọi là Docker layer caching. Docker coi mỗi dòng lệnh ta khai báo ở trong Dockerfile như là 1 instruction – chỉ dẫn, các instruction RUN, COPY, ADD
tạo ra các layer.
Khi ta build image, docker sẽ đọc từng dòng trong Dockerfile, nếu thấy layer nào ko thay đổi, đã có từ những lần build trước thì Docker sẽ tận dụng luôn chứ ko chạy lại nữa, do vậy nếu ta biết cách tổ chức Dockerfile, đưa các thành phần ít thay đổi lên trên, thành phần hay thay đổi xuống dưới thì sẽ tạn dụng được tính năng tuyệt vời này và giảm đáng kể thời gian ngồi chờ build image.
Và để làm được điều này thì sẽ thật tuyệt vời nếu ta có sự hiểu biết về project mà chúng ta đang dockerize (đây là lí do vì sao mình thấy application developer – frontend/backend mà biết thêm docker thường viết Dockerfile “có vẻ” xịn hơn mấy a chuyên Devops, vì họ thực sự hiểu cái họ đang làm, app của họ cần gì, cái gì có thể lược bỏ…, sorry các a Devops e ko có ý kì thị ).
Ta cùng phân tích 1 chút nhé:
- bài này project của chúng là NextJS – cũng là project javascript như bao project khác, thành phần hay thay đổi nhất đó là source code, còn dependencies (trong
package.json
sẽ ít thay đổi hơn nhiều, không phải lúc nào ta cũng cài thêm package (đâu nhỉ ). - thế nhưng khi nhìn lại Dockerfile, ta thấy rằng bước
COPY . .
copy toàn bộ source từ bên ngoài vào trong container lại được đặt ngay trên đầu. Dẫn tới việc khi có bất kì thay đổi nào trong source code thì toàn bộ các bước từ đó trở về sau phải chạy lại, mà các bạn biết đấy, project frontend càng to thìnpm install (yarn install)
sẽ càng ngày càng nặng nề, mất rất nhiều tgian, đặc biệt các bạn trên windows nữa, chạy rất lâu
Từ những quan sát đó ta tổ chức lại Dockerfile 1 chút như sau:
- Ban đầu chỉ cần copy file
package.json
vàyarn.lock
vào trong container để chạyyarn install
là đủ để ta cónode_modules
- Sau đó ta hẵng copy source code vào.
- Vì bước build và start luôn cần làm sau bước copy source code nên ta không thể thay đổi sự tình hơn được nữa
Âu cây ta bắt đầu làm thôi, đầu tiên các bạn thêm vào pages/_app.tsx
1 dòng console.log
nữa nhé:
console.log('Hello world')
console.log('Nice to meet you')
console.log('My name is James')// -> thêm dòng này vào, đổi thành tên của bạn nhé ;)
Sau đó các bạn update lại Dockerfile với nội dung như sau nhé:
FROM node:14-alpineWORKDIR /appCOPY package.json yarn.lock ./RUN yarn installCOPY . .RUN yarn buildCMD ["yarn", "start"]
Phần code bên trên chắc mình không cần giải thích lại đâu nhỉ
Sau đó ta tiến hành build lại image thôi nhé:
docker build -t test-nextjs .
Các bạn để ý rằng vì ta vừa sửa gần như toàn bộ Dockerfile nên lần đầu tiên build này thì tốc độ vẫn chậm.
Tiếp theo đó, ta quay lại code (pages/_app.tsx
)
, tiếp tục sửa:
console.log('Hello world')
console.log('Nice to meet you')
console.log('My name is James')
console.log('How are you')// thêm dòng này
Sau đó ta build lại image và để ý terminal sẽ thấy như sau:
Như các bạn thấy trên hình, Docker đã tận dụng CACHED
cho tất cả các instruction
phía trước dòng COPY
, nhờ thế ta không cần phải chờ để chạy lại những thứ mà không thực sự thay đổi nữa. 🤩🤩
Vì đây là project demo nên trong Dockerfile cũng ko có gì nhiều lắm, nhưng ở các project thật, khi mà ta có hàng loạt thứ phải làm trong Dockerfile, thời gian build hoàn chỉnh rất lâu, đó là khi docker layer caching lên tiếng
Mình luôn khuyến khích các bạn luôn chú trọng việc tổ chức Dockerfile sao cho quá trình build được tối ưu, cho ra những image chất lượng, thay vì chỉ “làm cho nó chạy là được”. Ta sẽ cùng thảo luận tiếp ở các phần sau trong bài này nhé.
Tận dụng cache từ image có sẵn
Lấy 1 ví dụ cụ thể xảy ra chính với mình:
- khi mình build docker image trên github action, bởi vì mỗi lần build là 1 môi trường mới hoàn toàn, nên docker layer caching trở nên vô dụng, vì trước đó trên môi trường đó ta đã build image được lần nào đâu mà có cache
- hoặc khi mình setup github Runners trên Kubernetes (K8S), cũng để build image thì vì 1 Kubernetes Cluster nó bao gồm nhiều node, nên nếu giữa 2 jobs của mình ko được chạy trên cùng 1 node thì docker layer caching cũng ko đc tận dụng. nếu “may mắn” 2 job liên tục chạy trên cùng 1 node thì mới được. Tỉ lệ đó càng ít hơn nếu như ta có càng nhiều node.
Việc lựa chọn build image trên K8S cluster về sau còn giúp mình nhận ra đó là 1 thảm hoạ vì các vấn đề râu ria nó gây ra cho cluster của mình luôn ấy. Thôi mình ko chia sẻ vào đây vì hơi lan man, bạn nào muốn tìm hiểu thêm thì ping mình nhé
Ở cả 2 ví dụ trên ta thấy rằng, trong các môi trường CICD thì việc tận dụng được docker layer caching có vẻ ít khả thi vì trong môi trường CICD thường ta luôn làm sao để có được môi trường clean – sạch sẽ nhất cho mỗi lần chạy.
Việc chờ đợi này sẽ thật sự gây khó chịu trong các project lớn dần, nhiều người, code push liên tục.
Và thật may Docker support cho chúng ta một tính năng cũng rất hữu dụng nữa đó là truyền vào option --cache-from
khi build Docker image, ở đó ta có thể chỉ định rõ 1 image nào đó làm “gốc” để khi build Docker sẽ dựa vào đó và bỏ qua các phần nào ko cần thiết.
Okay ta cùng bắt đầu thôi nhé. Đầu tiên các bạn giúp mình tạo 1 repo mới trên Gitlab, bạn nào chưa có tài khoản gitlab thì tạo 1 cái nhé. Đặt tên repo là docker-cache-from cho dễ gợi nhớ tới những gì ta sắp làm (dù nghe hơi chuối )
Sau đó ta copy folder docker-optimize-image ra một nơi nào đó bất kì trên máy của các bạn để push lên repo mới nhé.
Ta chạy lần lượt các command sau để commit folder kia lên repo gitlab:
git init
gitadd.git commit -m "first commit"# thay tên username của các bạn vào nhégit remote add origin https://gitlab.com/maitrungduc1410/docker-cache-from.git
git push -u origin master
Sau khi push xong nhớ quay lại gitlab kiểm tra là code của các bạn đã có trên đó rồi nhé (branch master nhé, nhiều lúc nó show branch main
đó )
Tiếp theo đây ở local, ta tạo file .gitlab-ci.yml
để cấu hình gitlab CICD và build image trên đó nhé:
image: docker:20services:- docker:20-dind
stages:- build
build:stage: build
before_script:- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME || true
->
docker build
--cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
.- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Tag thêm 1 image với tag là branch hiện tại để làm cache cho các lần build sau ($CI_COMMIT_REF_NAME = branch hiện tại)- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
Ở trên trông có vẻ khá quen thuộc ở các bài về CICD mình đã làm trước đó phải không các bạn
Về cơ bản những thứ có trong file cấu hình bên trên là: trước khi build image thì pull image từ registry về trước (nếu có), sau đó lúc build thì cache-from
cái image mình vừa pull về để đỡ phải chạy lại các layer nếu không có sự thay đổi gì, cuối cùng là push image lên lại registry (của Gitlab)
Nom có vẻ ổn rồi đó, ta commit code lên và ngồi xem thôi 😎 😎
git add .
git commit -m "ci: add gitlab cicd"
git push origin master
Đảm bảo là sau khi commit các bạn thấy pipeline đang chạy rồi nhé:
Chờ vài phút, các bạn sẽ thấy job báo hoàn tất:
Click vào xem chi tiết ta để ý thấy rằng, vì đây là lần build đầu tiên, chưa có image nào được push lên registry trước đó, nên bước ta pull image trước khi build về không có tác dụng gì cả
và Docker phải thực hiện chạy qua toàn bộ tất cả các layer:
Tiếp tục ta bấm vào nút Retry phía góc trên bên phải màn hình để chạy lại job này:
Lại chờ cho job chạy xong, ta để ý kết quả như sau:
Ta để ý thấy rằng thời gian để build job thứ 2 đã giảm đi đáng kể. Click vào job thứ 2 đó và xem thì ta sẽ thấy rằng là Docker đã dùng lại các layer của image ta pull về ngay trước đó làm cache, và không tính toán lại các layer đó nữa:
Nhờ đó thời gian build đã được rút ngắn đi
Giải pháp này áp dụng được cho tất cả các môi trường dù là Gitlab, Github hay Jenkins, Circle CI,… và thường mình luôn dùng cách này để giảm thời gian chờ đợi chạy CICD cả.
Note nếu bạn đang dùng BUILDKIT
Thời điểm hiện tại (tháng 9 – 2021) khi cài Docker về máy thì mặc định khi chạy docker build...
là Docker sẽ dùng BUILD KIT để build với nhiều tính năng mới, mình khuyến khích các bạn luôn dùng BUILDKIT để build image để image được tối ưu hơn và thời gian build giảm đi hơn nữa.
Nhưng cũng có 1 note nhỏ các bạn cần lưu ý nếu muốn dùng --cache-from
với BUILDKIT.
Đó là khi build image thì các bạn phải thêm tham số (argument) BUILDKIT_INLINE_CACHE=1
vào thì image của bạn mới có thể được dùng làm “gốc” cho các lần build tiếp theo nếu như bạn muốn dùng BUILDKIT. Nghe khó hiểu nhỉ .
Đơn giản là bạn update lại command build image như sau giúp mình là được:
# gitlab-ci.yml...->
DOCKER_BUILDKIT=1 docker build
--build-arg BUILDKIT_INLINE_CACHE=1
--cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
.
Tối giản size của Docker image
Mình để ý thấy rất nhiều ae thường chỉ viết Dockerfile để… cho nó chạy được 😅. Nhiều khi chỉ chỉ dừng lại ở ngay bước đầu tiên trong bài này là dừng lại và roll ra production
Làm vậy cũng chạy được app rồi, nhưng mình nghĩ là mình luôn có thể tạo được image optimize hơn rất nhiều, là tốt nhất để chạy production thì tại sạo lại ko làm chứ nhỉ? phải ko các bạn
Vấn đề size của image lớn có thực sự ảnh hưởng tới performance của app hay không thì mình thấy đây là vấn đề khá là gây tranh cãi . Nhưng từ thực tế của bản thân thì mình thấy một số điều như sau nếu image của bạn có size lớn:
- Tốn nhiều bộ nhớ lưu trữ (đương nhiên rồi )
- Thời gian startup time (khởi động container) và shutdown time (tắt container) khá lâu, thậm chí lắm lúc treo luôn, rất khó chịu. Mình cảm nhận thấy rõ rệt khi chạy các image 4-5GB
- Build image sẽ lâu hơn, push image lên registry cũng lâu hơn. Lúc build dễ bị failed xảy ra lỗi
No space left on device
mặc dù check thì còn cả mấy trăm GB bộ nhớ - Thi thoảng “không may” mình có để ý thấy sự chậm chạp nếu so sánh với các image size nhỏ 300-400MB
Nói chung mình thấy cái gì lớn quá cũng không tốt (yo, đầu óc sáng lên nhé các bạn, mình ko nghĩ đen tối gì đâu ). Và nếu mình có thể build được 1 image nhỏ gọn nhẹ thì tại sao lại không?
Ta cùng bắt đầu nhé.
Đầu tiên ta kiểm tra size của image hiện tại xem nó đang dư lào, các bạn chạy command:
docker images
Ta thấy in ra như sau:
Hiện tại size image của mình cỡ 700MB cũng “tương đối”
khi push lên registry thường nó sẽ được nén lại size thực tế trên registry cỡ bằng 1/2->1/3 (250MB)
giờ ta cùng ngồi phân tích lại 1 chút app của chúng ta cái gì thực sự cần khi chạy production nhé:
- mở
package.json
ta thấy rằng có rất nhiều packages ở đó, nhưng thực tế, sau bướcyarn build
thì số package ta thực tế cần không nhiều như thế, nhiều package -> node_modules sẽ to, thậm chí rất to -> size image to - Sau khi đã
yarn build
project thì cái ta thực tế cần chỉ là folder.next
haypublic
vànode_modules
mà thôi, các folder khác nhưpages
,lib
… và các file râu ria (.eslint, .prettier…) không cần nữa - Ta chỉ cần full node_modules tại thời điểm trước lúc
yarn build
thôi, sau đó thì vì ko cần nhiều package nữa nên ta chỉ cầnnode_modules
dạng tí hon thôi
Âu cây, với những phân tích như vậy thì ta sẽ tiến hành 1 số bước như sau để tổ chức lại Dockerfile:
- Hiện tại tất cả mọi package trong
package.json
đang được đặt ởdependencies
, ta tách ra cái nào cần cho lúc dev ở local thì đưa nó vàodevDependencies
, lát nữayarn build
xong thì loại bỏ nó khỏinode_modules
- chia Dockerfile ra thành nhiều stages, giữa các stage ta chỉ COPY những thứ thật cần thiết của stage trước đó làm “gốc” cho stage hiện tại
Ô xờ kê, đầu tiên ta xử lý em package.json
trước nhé, các bạn update lại với nội dung như sau:
{"name":"nextjs-commerce","version":"1.0.0","scripts":{"dev":"NODE_OPTIONS='--inspect' next dev","build":"next build","start":"next start","analyze":"BUNDLE_ANALYZE=both yarn build","lint":"next lint","prettier-fix":"prettier --write .","find:unused":"npx next-unused","generate":"graphql-codegen","generate:shopify":"DOTENV_CONFIG_PATH=./.env.local graphql-codegen -r dotenv/config --config framework/shopify/codegen.json","generate:vendure":"graphql-codegen --config framework/vendure/codegen.json","generate:definitions":"node framework/bigcommerce/scripts/generate-definitions.js"},"sideEffects":false,"license":"MIT","engines":{"node":">=14.x"},"dependencies":{"@react-spring/web":"^9.2.1","@vercel/fetch":"^6.1.0","autoprefixer":"^10.2.6","body-scroll-lock":"^3.1.5","classnames":"^2.3.1","cookie":"^0.4.1","email-validator":"^2.0.4","immutability-helper":"^3.1.1","js-cookie":"^2.2.1","keen-slider":"^5.5.1","lodash.debounce":"^4.0.8","lodash.random":"^3.2.0","lodash.throttle":"^4.1.1","next":"^11.0.0","next-seo":"^4.26.0","next-themes":"^0.0.14","postcss":"^8.3.5","postcss-nesting":"^8.0.1","react":"^17.0.2","react-dom":"^17.0.2","react-fast-marquee":"^1.1.4","react-merge-refs":"^1.1.0","react-use-measure":"^2.0.4","swell-js":"^4.0.0-next.0","swr":"^0.5.6","tabbable":"^5.2.0","tailwindcss":"^2.2.2","uuidv4":"^6.2.10"},"devDependencies":{"@graphql-codegen/cli":"^1.21.5","@graphql-codegen/schema-ast":"^1.18.3","@graphql-codegen/typescript":"^1.22.2","@graphql-codegen/typescript-operations":"^1.18.1","@next/bundle-analyzer":"^10.2.3","@types/body-scroll-lock":"^2.6.1","@types/cookie":"^0.4.0","@types/js-cookie":"^2.2.6","@types/lodash.debounce":"^4.0.6","@types/lodash.random":"^3.2.6","@types/lodash.throttle":"^4.1.6","@types/node":"^15.12.4","@types/react":"^17.0.8","deepmerge":"^4.2.2","eslint":"^7.31.0","eslint-config-next":"^11.0.1","eslint-config-prettier":"^8.3.0","graphql":"^15.5.1","husky":"^6.0.0","lint-staged":"^11.0.0","postcss-flexbugs-fixes":"^5.0.2","postcss-preset-env":"^6.7.0","prettier":"^2.3.0","typescript":"4.3.4"},"husky":{"hooks":{"pre-commit":"lint-staged"}},"lint-staged":{"**/*.{js,jsx,ts,tsx}":["eslint","prettier --write","git add"],"**/*.{md,mdx,json}":["prettier --write","git add"]},"next-unused":{"alias":{"@lib/*":["lib/*"],"@assets/*":["assets/*"],"@config/*":["config/*"],"@components/*":["components/*"],"@utils/*":["utils/*"]},"debug":true,"include":["components","lib","pages"],"exclude":[],"entrypoints":["pages"]}}
Chắc các bạn đang thắc mắc cái nội dung trên làm sao mình biết được cái nào nên đặt vào devDependencies
cái nào không, thì nội dung file trên là nội dung gốc lấy từ Github của Next Commerce , ban đầu mình chủ ý đưa hết nó vào dependencies
để giải thích cho các bạn phần này
Như các bạn thấy việc đặt cái nào vào devDependencies
, cái nào vào dependencies
nó cần có sự chú ý sắp xếp của người install cái package đó, thường ngay tại thời điểm install nó là ta đã có thể biết nó được dùng cho dev hay dùng cho cả production mode rồi đúng ko nào . Và cái này thì ai không thực sự code trong project khó mà biết được. Do vậy mình mới bảo vừa code được frontend/backend vừa biết deploy thì nó lợi thế nào
Tiếp theo ta tổ chức lại Dockerfile nhé:
# Install dependencies only when neededFROM node:14-alpine AS depsWORKDIR /appCOPY package.json yarn.lock ./RUN yarn install --frozen-lockfile# Rebuild the source code only when neededFROM node:14-alpine AS builderWORKDIR /appCOPY . .COPY /app/node_modules ./node_modulesRUN yarn build && yarn install --production --ignore-scripts --prefer-offline# Production image, copy all the files and run nextFROM node:14-alpine AS runnerWORKDIR /appENV NODE_ENV productionRUN addgroup -g 1001 -S nodejsRUN adduser -S nextjs -u 1001COPY /app/public ./publicCOPY /app/.next ./.nextCOPY /app/node_modules ./node_modulesCOPY /app/package.json ./package.jsonUSER nextjsCMD ["yarn", "start"]
Ta cùng xem bên trên có gì nhé:
- Ta có tất cả 3 stages: deps, builder và runner.
- deps là stage ta chả làm gì khác ngoài chạy
yarn install
mục đích là để ta có được foldernode_modules
(full node_modules đầy đủ mọi package ở cảdevDependencies
vàdependencies
). - builder: ở đây ta sẽ lấy folder
node_modules
từ stage deps và tiến hành build project, ngay sau khi build ta cũng chạy lạiyarn install
1 lần nữa với option--production
ý bảo yarn là chỉ giữ lại những package nào được khai báo ởdependencies
còn cái nào thuộcdevDependencies
thì loại hết nó ra khỏinode_modules
(bước này giảm size đi đáng kể đó ) - runner: ở bước này thì đơn giản là ta COPY lấy các thành phần thật sự cần thiết cho production từ stage builder và chạy project lên. Ở đây ta cũng tạo user
nextjs
vớiUID:GID=1001:1001
để chạy project, mục tiêu là luôn dùng user non-root để chạy app production nhé các bạn . Xem bài trước mình đã giải thích lí do vì sao nhé
Có vẻ ổn rồi đó ta tiến hành build lại image thôi, ta để tag là production
để lát ta so sánh với image hiện tại nhé:
docker build -t test-nextjs:production .
Chờ 1 lúc cho image build xong, lên Tiktok quét tranh thủ xem các idol trên đó có clip mới hay chưa
Sau khi image build thành công, ta cùng check lại size image mới xem nhé:
docker images
Và đây là kết quả:
Size chỉ còn 1/3 , quá tuyệt vời
Chỉ sau 1 vài “đường quyền” ta đã biến quả Dockerfile nặng nề ban đầu ra được 1 Dockerfile tốt hơn hẳn cho production, tối giản, nhẹ nhàng, vẫn tận dụng được docker layer caching, chia thành nhiều stages (chạy được cho cả lúc dev ở local luôn – stage deps
)
các bạn nhớ chạy thử image
production
lên và truy cập từ trình duyệt để đảm bảo là nó chạy ngon nhé
Kết bài
Qua bài này hi vọng rằng các bạn có được cái nhìn kĩ hơn về cách để build được những image chất lượng cho production.
Cùng với đó ta thấy được ích lợi nếu như các bạn đang là frontend/backend/fullstack developer và phải dockerize cho project mà các bạn đang làm, các bạn có lợi thế rất lớn vì bạn là người trực tiếp làm, hiểu rõ project của các bạn làm gì, cần gì, cái nào cần khi dev cái nào có thể lược bỏ cho production build,…Nếu chỉ đưa project cho bên DevOps deploy và đưa họ cái README đi chăng nữa thì đôi khi họ không có đủ context và họ không thể đưa ra solution tốt nhất được (cái này mình gặp thường xuyên luôn khi mình đưa project team mình cho bên devops deploy )
Chúc các bạn cuối tuần ngủ ngon và hẹn gặp lại ^^
P/s: source code kết quả cho bài này mình để ở branch complete-tutorial nhé
Nguồn: viblo.asia