Dockerize ứng dụng VueJS, ReactJS
Cập nhật gần nhất: 10/11/2024
Chào mừng các bạn đã quay trở lại với series học Docker và CICD của mình 👋👋
Ở bài trước chúng ta đã cùng nhau dockerize một ứng dụng Python cùng với đó là tìm hiểu về cách tạo biến môi trường trong Docker (một thứ rất hay dùng) và các để ta đẩy image ở local lên một registry (nơi lưu trữ Docker image, tựa như Github/Gitlab lưu code), đồng thời các mà ta download image về và chạy ở bất kì nơi nào ta muốn.
Ở bài này ta sẽ tiếp tục series bằng cách thực hành dockerize ứng dụng VueJS và ReactJS nhé.
Tại sao lại dockerize 2 project trong 1 bài, vì cách Dockerize VueJS và ReactJS khá là giống nhau (nếu không nói là y như nhau 😄). Nên mình sẽ gộp vào 1 bài luôn. Cùng với đó là một số kiến thức về Docker sẽ có ở trong bài.
Các bài trước dù đã cố gắng nhưng mỗi bài vẫn có khá nhiều khái niệm gửi tới các bạn, nếu có gì vẫn còn vương vấn thắc mắc thì các bạn cứ comment ở các bài đó cho mình được biết nhé.
Bài này sẽ đơn giản hơn đỡ đau mắt hơn đó 😂😂
Chúng ta cùng bắt đầu nhé 🚀🚀
Setup
Các bạn clone source code ở đây
Ở bài này ta sẽ chỉ cần quan tâm tới 2 folder trong source code bên trên đó là docker-vue và docker-react nhé
Ở 2 thư mục đó với tên tương ứng mình đã tạo sẵn cho các bạn 1 project VueJS và một project ReactJS
Dockerize Project VueJS
Đầu tiên chúng ta sẽ tiến hành dockerize project VueJS nha 💪
Build Image
Và vẫn như các bài trước để dockerize một project trước hết chúng ta cần phải cấu hình Dockerfile để định nghĩa Image với môi trường và những thứ cần thiết cho project
Cấu hình Dockerfile
Ở folder docker-vue các bạn tạo file tên là Dockerfile với nội dung như sau:
# build stage
FROM node:22-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
## các bạn có thể dùng yarn install .... tuỳ nhu cầu nhé
# production stage
FROM nginx:1.27-alpine as production-stage
COPY /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
Ồ bài này sao lại có 2 cái FROM
vậy nhỉ?
Như ở bài dockerize ứng dụng NodeJS phần những câu hỏi liên quan mình cũng đã giải thích là chúng ta có thể FROM nhiều lần trong 1 file Dockerfile nhé.
Thế tại sao bài này lại có. Bắt nguồn từ điều này: đối với project Vue hoặc React là dạng full frontend, không có tí backend nào, chúng chỉ được chạy trên trình duyệt. Mà khi ra tới trình duyệt, thì thứ trình duyệt hiểu chỉ là HTML, CSS, JS.
Do đó để Dockerize ứng dụng Vue/React việc của ta là chỉ cần lấy được những file build cuối cùng cần thiết để chạy ở trình duyệt còn những thứ khác có hay không có, không quan trọng 😆, làm như thế thì Image của chúng ta sẽ giảm được size xuống, giảm thiểu tối đa những thứ không cần thiết bên trong Image
Mình sẽ cùng nhau phân tích file Dockerfile trên để thấy những điều mình huyên thuyên bên trên nó là thế nào nha 😘:
- Ở file Dockerfile chúng ta chia làm 2 stage (giai đoạn) khi build image: build stage và production stage
- Ở build stage ta bắt đầu từ image tên là node:22-alpine, nếu các bạn không biết nó là gì thì đọc bài dockerize ứng dụng NodeJS của mình nhé. Để đặt tên cho từng giai đoạn ta dùng từ khoá as
- Trong build stage ta bắt đầu từ đường dẫn /app, sau đó copy toàn bộ file ở folder hiện tại ở môi trường ngoài, tức folder docker-vue vào bên trong đường dẫn ta set ở WORKDIR tức /app bên trong image.
- Tiếp theo ta chạy npm install như thường lệ để cài dependencies và cuối cùng là build project
- Ok build xong giờ tiến tới production stage: nơi ta định nghĩa cách chạy project
- Ở production stage ta bắt đầu với image tên là nginx.... đặt tên stage này là production-stage với từ khoá as
- Thế nginx ở đây là cái gì thế ???? Project VueJS hay ReactJS khi chạy sẽ cần một webserver để có thể chạy được nó, và Nginx ở đây chính là webserver. Ở local có cần Nginx gì đâu nhỉ? Vì ở local khi chạy npm run dev thì các nhà phát triển VueJS đã thiết lập sẵn cho chúng ta 1 local webserver rồi nhé, nhưng khi chạy thật thì không nên dùng nhé, phải có 1 webserver xịn, và Nginx thì rất là xịn nhé 😄😄
- Sau đó, phần này quan trọng nè, ta COPY từ build-stage lấy folder ở đường dẫn app/dist chính là "những file build cuối cùng cần thiết để chạy ở trình duyệt", ta lấy folder đó và copy vào đường dẫn /usr/share/nginx/html, đây chính là nơi Nginx sẽ tìm tới và trả về cho user khi user truy cập ở trình duyệt
- Và cuối cùng ta có CMD khởi động Nginx
Bài này có sự xuất hiện thêm của đối tượng lạ tên là Nginx có thể sẽ làm cho các bạn khó hiểu ngay từ bước đầu, nhưng "take it easy", cứ bình tĩnh và tạm coi nó là "1 anh bạn nào đó" cần thiết để làm nhiệm vụ phục vụ VueJS đến user, vì tự bản thân Vue không "show hàng" cho user xem được nhé 😉
Tạo .dockerignore
Các bạn tạo tiếp cho mình file .dockerignore
với nội dung như sau:
node_modules
dist
Ở trên ta nói với Docker rằng đừng có copy folder node_modules
(nếu có) trong quá trình build image nhé, bởi vì làm như vậy image của ta sẽ rất nặng và có thể xảy ra lỗi Canceled: context canceled
lúc build
Build Image
Và để build image thì vẫn như một cái thường lệ từ các bài trước , ta chạy command sau:
docker build -t learning-docker/vue:v1 .
Nếu các bạn có thắc mắc command trên làm gì thì xem lại các bài trước trong series này nhé
Chạy project
Để chạy project ta lại "rất như thường lệ các bài trước" lại tại một file tên là docker-compose.yml với nội dung như sau:
services:
app:
image: learning-docker/vue:v1
ports:
- "5000:80"
restart: unless-stopped
Những thứ bên trên cũng được mình đã giải thích ở các bài trước rồi nhé các bạn 😉, Lại "rất như thường lệ" nếu các bạn không hiểu thì các bạn xem lại các bài trước nha 😘
"TỪ! Ở trên cổng 80
lấy ở đâu ra vậy anh zai viết blog hê" 🤔
Thì mặc định của Nginx khi bạn ý chạy thì bạn ý sẽ lắng nghe ở cổng 80 đó các bạn 😝, dù trong Dockerfile ta không có, cái đó được setup ở trong Nginx rồi nhé ta chưa cần quan tâm.
Cuối cùng là chạy project lên thôi nào. Lại "rất như thườ....." (thôi không nói nữa), ta chạy command sau:
docker compose up -d
Sau đó mở trình duyệt ở địa chỉ localhost:5000 và xem nhé:
Cho bạn nào xem luôn bài này và không hiểu cổng 80 và 5000 nó là cái gì thì các bạn nên xem các bài trước trong series này nha 😘
Dockerize project ReactJS
Với project ReactJS thì sự giống nhau là 99,99% so với VueJS nhé, vì ở bài này mình tạo project React + Vue bằng Vite, nên mọi cấu hình gần như y hệt, file Dockerfile cho React sẽ giống y hệt như sau:
# build stage
FROM node:22-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
## các bạn có thể dùng yarn install .... tuỳ nhu cầu nhé
# production stage
FROM nginx:1.27-alpine as production-stage
COPY /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
nhớ tạo cả file
.dockerignore
như bên Vue nữa nha
Phần này các bạn thử tự làm và xem kết quả có ra được ngon lành như Vue không nhé 😉
Ta build với command:
docker build -t learning-docker/react:v1 .
Sau đó tạo docker-compose.yml
:
services:
app:
image: learning-docker/react:v1
ports:
- "5001:80"
restart: unless-stopped
Kết qủa như sau:
Có cách nào để không phải chia Dockerfile thành 2 stage?
Thì câu trả lời là có, về sau khi mình hiểu được cách này thì mình toàn dùng cách này cả 😄.
Bắt đầu thôi nào....💪💪
Trước hết ta sẽ cần lắc não một chút:
- Như ở trên mục đích của chúng ta là cần phải build được project, sau đó lấy "những file build cuối cùng" tức là lấy được folder dist và gửi nó tới Nginx và bảo "Nginx cậu show hàng hộ tớ nếu có user hỏi thăm đến tớ" 😂😂
- Vậy vấn đề ở đây là làm cách nào có thể build được project mà không cần dùng thêm một stage như ở Dockfile, sau đó chuyển nó tới cho anh bạn thân Nginx là ok rồi 😘
Setup
Docker hỗ trợ chúng ta có thể tạo ra một "container tạm thời" (intermediate container, từ này dịch ra phải là trung gian mới đúng như nghe nó không hay 🤣).
Container này sau khi làm xong nhiệm vụ thì sẽ tự được xoá đi.
Ta sẽ dùng container này để build project nha
Bắt đầu thôiiiiiii...
Trước hết các bạn tắt project nếu đang chạy bằng docker-compose đi nhé:
docker compose down
Vẫn ở folder docker-vue chạy command sau:
# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build
# Nếu bạn đang dùng Windows thì command trên sẽ như sau:
# Ta phải tách ra chạy 2 command riêng
# Với Git bash
docker run --rm -v "/$(pwd)":/app -w //app node:22-alpine npm install
docker run --rm -v "/$(pwd)":/app -w //app node:22-alpine npm run build
# Với PowerShell
docker run --rm -v "$(pwd):/app" -w /app node:22-alpine npm install
docker run --rm -v "$(pwd):/app" -w /app node:22-alpine npm run build
# Với Command Prompt
docker run --rm -v "%cd%:/app" -w /app node:22-alpine npm install
docker run --rm -v "%cd%:/app" -w /app node:22-alpine npm run build
Giải thích nè:
- Ở câu lệnh trên ta chạy câu lệnh docker tạo ra 1 container với option --rm ý bảo "chạy xong chú tự xoá đi nhé"
- Tiếp theo ta có option -v tức là volume, ồ volume là gì mới à nha. Ở bài đầu tiên mình đã nói tới rồi nhé, các bạn cứ bình tĩnh dần dần ta sẽ học nó nhiều hơn
- Sau -v là
$(pwd):/app
, ở đây ý ta bảo là đưa toàn bộ file ở folder hiện tại ở môi trường gốc ánh xạ vào trong đường dẫn/app
trong Image ($(pwd)
trả về đường dẫn hiện tại) , việc này gọi đúng thuật ngữ thì là mount - Tiếp theo ta có option -w chính là WORKDIR nhé
- Tiếp theo ta có node:22-alpine: tương đương với FROM node:22-alpine
- Sau đó là các command ta cần chạy để build project.
Các bạn chạy command trên nhé, có thể 1 lúc không thấy terminal in ra gì, đừng hoang mang nhé, 1 chút thôi là in ra cả một đống log đó 😂
Sau khi command trên chạy thành công thì các bạn sẽ thấy ở folder docker-vue xuất hiện cả folder node_modules và folder dist. Đây chính là điều mà volume trong Docker mang lại.
Volume giúp ánh xạ file ở môi trường ngoài vào trong Docker container, và ánh xạ này là ánh xạ 2 chiều, ngoài thay đổi thì trong thay đổi, trong thay đổi thì ngoài thay đổi theo. Đó là lí do vì sao khi command trên chạy xong ở bên ngoài ta lại có kết quả như vậy. Dần dần các bạn sẽ hiểu volume nó là gì nhé 😉
Ok vậy là giờ ta đã có folder dist rồi, nhiệm vụ tiếp theo là gửi nó tới nginx.
Các bạn sửa lại file docker-compose.yml như sau:
services:
app:
image: nginx:1.27-alpine
volumes:
- ./dist:/usr/share/nginx/html
ports:
- "5000:80"
restart: unless-stopped
Ở trên ta đã thay tên image bằng tên của image của Nginx, đồng thời ta dùng từ khoá volumes để ánh xạ nội dung bên trong folder dist vào trong folder /usr/share/nginx/html nơi Nginx cần. (nhớ là nội dung bên trong dist chứ không có folder dist đâu nhé)
Các bạn chú ý trong docker khi ta ánh xạ (hay gọi chính xác là mapping dùng cho port hay mount dùng cho volume), biêủ thức sẽ chia làm 2 vế thì vế trái luôn là ở môi trường gốc (bên ngoài), vế phải là bên trong container nhé
Và chú ý là khi ánh xạ volumes thì vế bên phải (nơi container) đường dẫn phải là đường dẫn tuyệt đối nhé.
Cuối cùng là ta chạy lại project xem sao nào:
docker compose up -d
Các bạn mở trình duyệt và sẽ lại thấy điều tương tự như trước đó ta đã thấy nha:
Ta thử chui vào container xem có gì nhé, các bạn mở terminal khác tại folder docker-vue và chạy command sau:
docker-compose exec app sh
cd /usr/share/nginx/html
ls -l
Và các bạn sẽ thấy như sau
Review
Tại sao với Vue hay React mình lại chọn cách này??
Các bạn để ý thấy là cuối cùng ở file docker-compose khi chạy ta dùng image nginx là image đã được build sẵn, chứ không còn dùng image learning-docker:vue nữa. Mà image learning-docker:vue cần phải build mới có thể dùng được. Do đó mỗi khi code Vue của ta thay đổi thì ta cần build lại Image. Tức là với cách dùng container tạm thời ta không cần dùng đến file Dockerfile nữa luôn 🎉🎉
Điều khác biệt chút chút là giờ ta chạy với command này thôi:
# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm install
docker run --rm -v $(pwd):/app -w /app node:22-alpine npm run build
...
Và cuối cùng là khởi động project được luôn
KHÔNGGGGGGG 😭😭, dùng cách này tự nhiên ở môi trường gốc bên ngoài của tôi lại có node_modules và dist, nó làm môi trường gốc của tôi không còn "trinh trắng" nữa. Cái gì bên trong Docker thì hay cứ ở bên trong đó đi...
Nếu việc ta mount từ folder bên ngoài vào trong container làm bên ngoài xuất hiện thêm nhiều file làm các bạn thấy không muốn thì các bạn có thể quay lại cách build Image với 2 stages như đầu bài ta đã làm.
Các bạn tuỳ chọn cách làm mà các bạn thấy thích hợp nhất nhé.
Nhưng các bạn ạ, cái gì "trong trắng" quá chưa hẳn tốt, vì thế mà có một số bộ phận thanh niên hiện nay có khẩu vị mặn đó là "fall in love" với các chị hơn tuổi từng trải 😂😂
Lúc nào cũng dùng cách này có được không?
Thì cách này theo mình thấy dùng được cho các project khi chạy hoặc build không cần cấu hình nhiều, các project chuyên về frontend, không dính dáng tí backend nào, kiểu React, Vue, Angular, Svelte,... đều chơi được
Còn các project có backend thì thường ta sẽ cần setup nhiều bước và nhiều thứ lằng nhằng nên cách này sẽ khó đáp ứng được
Thay đổi code bên ngoài F5 trình duyệt không thấy thay đổi????
Như các bạn có thể thấy ở trong bài khi build image, trong Dockerfile chúng ta chỉ chạy npm run build
để build app duy nhất 1 lần và dùng kết quả đó để chạy, do đó khi các bạn thay đổi code bên ngoài thì bên trong không đổi dẫn tới F5 trình duỵệt không thấy gì.
Thế vậy map volume toàn bộ code từ bên ngoài vào là được đúng không............. 🤔🤔
Khồnggggg, nếu ta chỉ map code từ bên ngoài vào thì khi ta thay đổi, code bên trong container cũng thay đổi, nhưng chúng không được build lại. Chúng ta phải làm sao chạy được npm run build
để nó "lắng nghe" code thay đổi và build lại bất kì khi nào ta đổi code. Các bạn làm như sau nhé.
Ở Dockerfile
của folder docker-vue
các bạn sửa lại toàn bộ như sau:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "run", "dev"]
Ở đây ta đã bỏ đi chỉ còn 1 chút tí tẹo, để chạy npm install
và chạy npm run dev
để theo dõi code thay đổi và tự động build lại nhé.
Tiếp theo ta cập nhật lại file vite.config.ts
:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: true, // thêm dòng này để app có thể được truy cập từ môi trường ngoài
watch: {
usePolling: true, // thêm dòng này để xử lý lỗi với WSL trên Windows
},
},
});
Tiếp theo ta build lại image nhé:
docker build -t learning-docker/vue:v1 .
Tiếp đó ở docker-compose.yml
các bạn sửa lại như sau:
version: "3.7"
services:
app:
image: learning-docker/vue:v1
volumes:
- ./src:/app/src
ports:
- "5000:8080"
restart: unless-stopped
Vì npm run dev
đã tạo sẵn một server dev ở cổng 5173
nên đó là lí do vì sao ta không cần tới nginx
làm webserver nữa, và như các bạn có thể thấy giờ ta map vào container port 5173
chứ không phải 80
như với nginx
nữa.
Sau đó ta start project:
docker compose up -d
Cuối cùng là mở trình duyệt và test thử nha:
Các bạn có để ý thấy là ta mount volume chỉ mỗi folder src
vào trong không. Tại sao ta không mount tất cả như sau:
services:
app:
image: learning-docker/vue:v1
volumes:
- .:/app # ---->> tại sao???
ports:
- "5000:5173"
restart: unless-stopped
Tại saooooooooooooooooooooooooooo? 🤔🤔
Lí do nếu ta mount toàn bộ folder bên ngoài vào trong thì vì bên ngoài không có node_modules
(bên ngoài ta vẫn còn trinh
và không có node_modules trước đó), nên tại thời điểm chạy, toàn bộ bên ngoài sẽ overridde bên trong dẫn tới bên trong mất node_modules
, vậy nên ta chỉ map cụ thể những folder cần thiết vào trong.
Ta test thử mount toàn bộ code bên ngoài vào trong container nhé, giả sử ta setup từ đầu, ở môi trường ngoài ta không có node_modules/dist gì cả:
Ta cập nhật lại docker-compose.yml
để mount toàn bộ folder từ môi trường ngoài vào trong container:
services:
app:
image: learning-docker/vue:v1
volumes:
- .:/app # ---->> ở đây
ports:
- "5000:5173"
restart: unless-stopped
Sau đó ta start:
docker compose down
docker compose up -d
Thì khi start lên, check logs ta sẽ thấy báo lỗi:
Thử exec
vào container:
docker compose exec app sh
>>>
Error response from daemon: Container 20d3c0a9753ac27138c9fecdba174ca0c8e94d3e72f4b0767653a1e841959958 is restarting, wait until the container is running
Thậm chí ta còn không exec
vào container được luôn vì nó failed và restart liên tục 😂
Để debug và exec vào được thì ta update lại docker-compose.yml
một chút để nó sleep
:
services:
app:
image: learning-docker/vue:v1
volumes:
- .:/app
ports:
- "5000:5173"
restart: unless-stopped
command: sleep 1000000
Ở trên các bạn để ý ta có command: sleep 1000000
, cái command
đó sẽ override lại cái ta có ở Dockerfile
. Sau đó ta start lại:
docker compose down
docker compose up -d
# Tiếp tục exec container
docker compose exec app sh
ls -la
Sau khi chạy ls -la
ta thấy như sau:
không có node_modules
, như mình đã nói từ trước đó
Cách fix sẽ như sau, ta update lại docker-compose.yml
:
services:
app:
image: learning-docker/vue:v1
volumes:
- .:/app
- /app/node_modules
ports:
- "5000:5173"
restart: unless-stopped
Các bạn để ý bên trên ta có thêm 1 dòng /app/node_modules
. Cái đó gọi là Anonymous Volume
(volume không tên tuổi 😂). Bằng cách này thì node_modules
bên trong container được tạo ra ở bước npm install
trong Dockerfile sẽ được isolate
và không bị override khi ta mount toàn bộ source code từ ngoài vào trong ở đoạn .:/app
Sau đó ta start lại project:
docker compose down
docker compose up -d
Và sẽ thấy mọi thứ lại oke 😘
Để ý rằng với cách này ở môi trường ngoài vẫn có folder node_modules
được tạo ra nhưng nó empty:
Còn trong container thì node_modules
vẫn data:
Cái
Anonymous volume
này vi diệu nhỉ 🤣🤣
Cá nhân mình thấy các này hơi rườm rà chút cho project Vue/React thuần, tức là chỉ có mỗi frontend. Thường project kiểu này mình dev như bình thường không dùng Docker, chỉ khi nào deploy trên server mới chạy Docker. Nhưng nếu bạn muốn giữ "zin" cho môi trường ngoài thì cách này cũng oke luôn
Kết bài
May quá cũng viết được một bài tạm gọi là đỡ dài dòng 😂😂
Ở bài này các bạn đã biết được cách để dockerize project VueJS và ReactJS như thế nào, và cách làm cũng rất là tương tự với những project "chuyên" frontend khác. Các bạn có thể tìm một project nào đó (Angular chẳng hạn) và thử dockerize xem nhé 😉
Qua bài này có một số nội dung quan trong như sau:
- Cách chia quá trình build Image thành nhiều giai đoạn (stage) và cách COPY các tài nguyên giữa các stage đó
- Cách khởi tạo "container tạm thời" để thực hiện một số tác vụ cần ít thao tác cấu hình
- Cơ bản chút chút về volumes để mount (ánh xạ) tài nguyên giữa môi trường gốc (bên ngoài) vào bên trong container
- Cùng với đó là chút chút về Nginx là gì, sau này ta sẽ dùng Nginx làm webserver/proxy khá là nhiều đó 😁
Cám ơn các bạn đã theo dõi series của mình, nếu có gì thắc mắc các bạn cứ comment cho mình biết nha.
Hẹn gặp lại các bạn ở các bài sau ^^
All rights reserved