Sổ tay Docker: Cách xây dựng ứng dụng JavaScript với Docker
Trong phần trước, bạn đã tìm hiểu về các thao tác Docker Image cơ bản. Nếu bỏ lỡ thì bạn có thể xem lại bài viết này ở dưới đây:
Bây giờ bạn đã có một số ý tưởng về cách tạo image, đã đến lúc làm việc với thứ gì đó phù hợp hơn một chút.
Trong phần này, bạn sẽ làm việc với mã nguồn của image fhsinchy/hello-dock mà bạn đã làm việc ở phần trước. Trong quá trình tạo image cho ứng dụng rất đơn giản này, bạn sẽ được giới thiệu về volume và các bản dựng nhiều giai đoạn, hai trong số các khái niệm quan trọng nhất trong Docker.
Cách tạo Dockerfile môi trường Development
Để bắt đầu, hãy mở thư mục mà bạn đã clone repository https://github.com/fhsinchy/docker-handbook-projects ở phần trước. Mã cho ứng dụng hello-dock
nằm bên trong thư mục con có cùng tên.
Đây là một dự án JavaScript rất đơn giản được cung cấp bởi dự án vitejs/vite. Tuy nhiên, đừng lo lắng, bạn không cần phải biết JavaScript hoặc Vite để xem bài viết này. Bạn chỉ cần hiểu cơ bản về Node.js và npm là đủ.
Cũng giống như bất kỳ dự án nào khác mà bạn đã thực hiện trong các phần trước, bạn sẽ bắt đầu bằng cách lập kế hoạch về cách bạn muốn ứng dụng này chạy. Theo tôi, kế hoạch nên như sau:
- Nhận một image cơ sở tốt để chạy các ứng dụng JavaScript, như Node.
- Thiết lập thư mục làm việc mặc định bên trong image.
- Sao chép tệp
package.json
vào image. - Cài đặt các phụ thuộc cần thiết.
- Sao chép phần còn lại của các tệp dự án.
- Khởi động máy chủ phát triển
vite
bằng cách thực thi lệnhnpm run dev
.
Kế hoạch này luôn phải đến từ nhà phát triển ứng dụng mà bạn đang container hóa. Nếu bản thân bạn là nhà phát triển, thì bạn nên hiểu đúng về cách ứng dụng này cần được chạy.
Bây giờ nếu bạn thiết lập kế hoạch được đề cập ở trên bên trong tệp Dockerfile.dev
, nó sẽ trông giống như sau:
FROM node:lts-alpine
EXPOSE 3000
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY ./package.json .
RUN npm install
COPY . .
CMD [ "npm", "run", "dev" ]
Giải thích cho mã này như sau:
- Chỉ dẫn
FROM
thiết lập image Node.js chính thức là image cơ sở, đem lại cho bạn những điều tuyệt vời của Node.js cần thiết để chạy bất kỳ ứng dụng JavaScript. Thẻlts-alpine
cho biết rằng bạn muốn sử dụng các biến thể Alpine, phiên bản mới nhất và hỗ trợ lâu dài của image. Các thẻ có sẵn và tài liệu cần thiết cho image có thể được tìm thấy trên trang Node. - Chỉ dẫn
USER
thiết lập người dùng mặc định cho image lànode
. Theo mặc định, Docker chạy các container với tư cách là người dùngroot
. Nhưng theo best practices của Docker và Node.js, điều này có thể gây ra mối đe dọa bảo mật. Vì vậy, tốt hơn hết bạn nên chạy với tư cách người dùng không phải root bất cứ khi nào có thể. Image Node đi kèm với một người dùng không phải root được đặt tên lànode
mà bạn có thể thiết lập làm người dùng mặc định bằng cách sử dụng chỉ dẫnUSER
. - Chỉ dẫn
RUN mkdir -p /home/node/app
tạo thư mụcapp
bên trong thư mục home của người dùngnode
. Thư mục home cho bất kỳ người dùng không phải root nào trong Linux thường là/home/<user name>
theo mặc định. - Sau đó, chỉ dẫn
WORKDIR
thiết lập thư mục làm việc mặc định thành thư mục mới được tạo/home/node/app
. Theo mặc định, thư mục làm việc của bất kỳ image nào là thư mục gốc. Bạn không muốn bất kỳ tệp không cần thiết nào bị rải khắp thư mục gốc của mình, phải không? Do đó, bạn thay đổi thư mục làm việc mặc định thành một cái gì đó hợp lý hơn như/home/node/app
hoặc bất cứ thứ gì bạn thích. Thư mục làm việc này sẽ được áp dụng cho bất kỳ chỉ dẫnCOPY
,ADD
,RUN
vàCMD
nào ở phía sau nó. - Chỉ dẫn
COPY
sao chép tập tinpackage.json
chứa các thông tin liên quan đến tất cả các phụ thuộc cần thiết cho ứng dụng này. Chỉ dẫnRUN
thực thi lệnhnpm install
, là lệnh mặc định để cài đặt các phần phụ thuộc bằng cách sử dụng tệppackage.json
trong các dự án Node.js. Dấu.
ở cuối đại diện cho thư mục làm việc. - Chỉ dẫn
COPY
thứ hai sao chép phần còn lại của nội dung từ thư mục hiện tại (.
) của hệ thống tệp máy chủ vào thư mục làm việc (.
) bên trong image. - Cuối cùng, chỉ dẫn
CMD
thiết lập lệnh mặc định cho image này lànpm run dev
được viết dưới dạngexec
. - Máy chủ phát triển
vite
theo mặc định chạy trên cổng3000
và thêm một lệnhEXPOSE
có vẻ là một ý tưởng hay, vậy là xong.
Bây giờ, để xây dựng image từ Dockerfile.dev
này, bạn có thể thực hiện lệnh sau:
docker image build --file Dockerfile.dev --tag hello-dock:dev .
# Step 1/7 : FROM node:lts
# ---> b90fa0d7cbd1
# Step 2/7 : EXPOSE 3000
# ---> Running in 722d639badc7
# Removing intermediate container 722d639badc7
# ---> e2a8aa88790e
# Step 3/7 : WORKDIR /app
# ---> Running in 998e254b4d22
# Removing intermediate container 998e254b4d22
# ---> 6bd4c42892a4
# Step 4/7 : COPY ./package.json .
# ---> 24fc5164a1dc
# Step 5/7 : RUN npm install
# ---> Running in 23b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 23b4de3f930b
# ---> c17ecb19a210
# Step 6/7 : COPY . .
# ---> afb6d9a1bc76
# Step 7/7 : CMD [ "npm", "run", "dev" ]
# ---> Running in a7ff529c28fe
# Removing intermediate container a7ff529c28fe
# ---> 1792250adb79
# Successfully built 1792250adb79
# Successfully tagged hello-dock:dev
Với tên tệp là Dockerfile
, bạn không phải truyền tên tệp một cách rõ ràng bằng cách sử dụng tùy chọn --file
. Một container có thể được chạy bằng cách sử dụng image này bằng cách thực hiện lệnh sau:
docker container run
--rm
--detach
--publish 3000:3000
--name hello-dock-dev
hello-dock:dev
# 21b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268
Bây giờ hãy truy cập http://127.0.0.1:3000
để xem ứng dụng hello-dock
đang hoạt động.
Chúc mừng bạn đã chạy ứng dụng thực tế đầu tiên của mình bên trong một container. Mã bạn vừa viết không sao nhưng có một vấn đề lớn với nó và một vài chỗ có thể được cải thiện. Hãy bắt đầu với vấn đề đầu tiên.
Cách làm việc với liên kết ràng buộc trong Docker
Nếu bạn đã làm việc với bất kỳ JavaScript front-end framework nào trước đây, bạn biết rằng các máy chủ phát triển trong các khung này thường đi kèm với tính năng tải lại nóng (hot reload). Đó là nếu bạn thực hiện thay đổi trong mã của mình, máy chủ sẽ tải lại, tự động phản ánh bất kỳ thay đổi nào bạn đã thực hiện ngay lập tức.
Nhưng nếu bạn thực hiện bất kỳ thay đổi nào trong mã của mình ngay bây giờ, bạn sẽ không thấy gì xảy ra với ứng dụng của mình đang chạy trong trình duyệt. Điều này là do bạn đang thực hiện các thay đổi trong mã mà bạn có trong hệ thống tệp cục bộ của mình nhưng ứng dụng bạn đang thấy trong trình duyệt lại nằm bên trong hệ thống tệp container.
Để giải quyết vấn đề này, bạn có thể sử dụng liên kết ràng buộc (bind mounts). Sử dụng liên kết ràng buộc, bạn có thể dễ dàng gắn kết một trong các thư mục hệ thống tệp cục bộ của mình vào bên trong một container. Thay vì tạo một bản sao của hệ thống tệp cục bộ, liên kết ràng buộc có thể tham chiếu hệ thống tệp cục bộ trực tiếp từ bên trong container.
Bằng cách này, bất kỳ thay đổi nào bạn thực hiện đối với mã nguồn cục bộ của mình sẽ phản ánh ngay lập tức bên trong container, kích hoạt tính năng tải lại nóng của máy chủ phát triển vite
. Các thay đổi được thực hiện đối với hệ thống tệp bên trong container cũng sẽ được phản ánh trên hệ thống tệp cục bộ của bạn.
Bạn đã tìm hiểu trong phần image thực thi, các liên kết ràng buộc có thể được tạo bằng cách sử dụng tùy chọn --volume
hoặc -v
cho các lệnh container run
hoặc container start
. Cú pháp chung như sau:
--volume <local file system directory absolute path>:<container file system directory absolute path>:<read write access>
Dừng container hello-dock-dev
đã khởi động trước đó của bạn và bắt đầu container mới bằng cách thực hiện lệnh sau:
docker container run
--rm
--publish 3000:3000
--name hello-dock-dev
--volume $(pwd):/home/node/app
hello-dock:dev
# sh: 1: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! [email protected] dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the [email protected] dev script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?
Hãy nhớ rằng, tôi đã bỏ qua tùy chọn --detach
và điều đó để chứng minh một điểm rất quan trọng. Như bạn có thể thấy, ứng dụng hiện không chạy.
Đó là bởi vì mặc dù việc sử dụng một ổ đĩa giải quyết được vấn đề tải lại nóng, nhưng nó lại gây ra một vấn đề khác. Nếu bạn đã có bất kỳ kinh nghiệm nào trước đây với Node.js, bạn có thể biết rằng các phần phụ thuộc của một dự án Node.js nằm trong thư mục node_modules
trên thư mục gốc của dự án.
Bây giờ bạn đang gắn thư mục gốc dự án trên hệ thống tệp cục bộ của mình dưới dạng một ổ đĩa bên trong container, nội dung bên trong container sẽ được thay thế cùng với thư mục node_modules
chứa tất cả các phần phụ thuộc. Điều này có nghĩa là gói vite
đã bị thiếu.
Cách làm việc với ổ đĩa ẩn danh trong Docker
Vấn đề này có thể được giải quyết bằng cách sử dụng một ổ đĩa ẩn danh. Một ổ đĩa ẩn danh giống hệt với một liên kết ràng buộc ngoại trừ việc bạn không cần chỉ định thư mục nguồn ở đây. Cú pháp chung để tạo một ổ đĩa ẩn danh như sau:
--volume <container file system directory absolute path>:<read write access>
Vì vậy, lệnh cuối cùng để khởi động container hello-dock
với cả hai ổ đĩa phải như sau:
docker container run
--rm
--detach
--publish 3000:3000
--name hello-dock-dev
--volume $(pwd):/home/node/app
--volume /home/node/app/node_modules
hello-dock:dev
# 53d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b
Tại đây, Docker sẽ lấy toàn bộ thư mục node_modules
từ bên trong container và giấu nó vào một số thư mục khác được quản lý bởi Docker daemon trên hệ thống tệp máy chủ của bạn và sẽ gắn thư mục đó vào node_modules
bên trong container.
Cách thực hiện xây dựng Image nhiều giai đoạn trong Docker
Cho phần này, bạn đã xây dựng một image để chạy một ứng dụng JavaScript ở chế độ phát triển (development). Bây giờ nếu bạn muốn xây dựng image ở chế độ sản xuất (production), một số thách thức mới sẽ xuất hiện.
Trong chế độ phát triển, lệnh npm run serve
khởi động một máy chủ phát triển phục vụ ứng dụng cho người dùng. Máy chủ đó không chỉ phục vụ các tệp mà còn cung cấp tính năng tải lại nóng.
Trong chế độ sản xuất, lệnh npm run build
biên dịch tất cả mã JavaScript của bạn thành một số tệp HTML, CSS và JavaScript tĩnh. Để chạy các tệp này, bạn không cần Node hoặc bất kỳ phụ thuộc thời gian chạy nào khác. Tất cả những gì bạn cần là một máy chủ, nginx
chẳng hạn.
Để tạo image trong đó ứng dụng chạy ở chế độ sản xuất, bạn có thể thực hiện các bước sau:
- Sử dụng
node
làm image cơ sở và xây dựng ứng dụng. - Cài đặt
nginx
bên trong image Node và sử dụng nó để phân phát các tệp tĩnh.
Cách làm này hoàn toàn hợp lệ. Nhưng vấn đề là image node
lớn và hầu hết những thứ mà nó mang theo là không cần thiết để phục vụ các tệp tĩnh của bạn. Một cách tiếp cận tốt hơn cho tình huống này như sau:
- Sử dụng image
node
làm cơ sở và xây dựng ứng dụng. - Sao chép các tệp được tạo bằng image
node
vào một imagenginx
. - Tạo image cuối cùng dựa trên
nginx
và loại bỏ tất cả những thứ liên quan đếnnode
.
Bằng cách này, image của bạn chỉ chứa các tệp cần thiết và trở nên thực sự tiện dụng.
Cách tiếp cận này chính là xây dựng Image nhiều giai đoạn. Để thực hiện một bản dựng như vậy, hãy tạo một tệp Dockerfile
mới bên trong thư mục dự án hello-dock
của bạn và đưa nội dung sau vào đó:
FROM node:lts-alpine as builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
EXPOSE 80
COPY --from=builder /app/dist /usr/share/nginx/html
Như bạn có thể thấy Dockerfile
này trông rất giống những cái trước của bạn với một vài điểm khác lạ. Giải thích cho tệp này như sau:
- Dòng 1 bắt đầu giai đoạn đầu tiên của bản dựng sử dụng image
node:lts-alpine
làm image cơ sở. Cú phápas builder
gán tên cho giai đoạn này để nó có thể được gọi đến sau này. - Từ dòng 3 đến dòng 13, đó là những thứ tiêu chuẩn mà bạn đã thấy nhiều lần trước đây. Chỉ dẫn
RUN npm run build
biên dịch toàn bộ ứng dụng và thu nhỏ nó bên trong thư mục/app/dist
,/app
là thư mục làm việc và/app/dist
là thư mục đầu ra mặc định cho các ứng dụngvite
. - Dòng 15 bắt đầu giai đoạn thứ hai của bản dựng sử dụng image
nginx:stable-alpine
làm image cơ sở. - Máy chủ NGINX chạy trên cổng 80 theo mặc định nên dòng
EXPOSE 80
được thêm vào. - Dòng cuối cùng là một chỉ dẫn
COPY
. Phần này--from=builder
chỉ ra rằng bạn muốn sao chép một số tệp từ vùng hiển thịbuilder
. Sau đó, đó là một chỉ dẫn sao chép tiêu chuẩn, với/app/dist
là thư mục nguồn và/usr/share/nginx/html
là thư mục đích. Thư mục đích được sử dụng ở đây là đường dẫn trang mặc định cho NGINX, vì vậy bất kỳ tệp tĩnh nào bạn đặt bên trong đó sẽ được tự động phân phát.
Như bạn có thể thấy, image kết quả dựa trên image cơ sở nginx
chỉ chứa các tệp cần thiết để chạy ứng dụng. Để xây dựng image này, hãy thực hiện lệnh sau:
docker image build --tag hello-dock:prod .
# Step 1/9 : FROM node:lts-alpine as builder
# ---> 72aaced1868f
# Step 2/9 : WORKDIR /app
# ---> Running in e361c5c866dd
# Removing intermediate container e361c5c866dd
# ---> 241b4b97b34c
# Step 3/9 : COPY ./package.json ./
# ---> 6c594c5d2300
# Step 4/9 : RUN npm install
# ---> Running in 6dfabf0ee9f8
# npm WARN deprecated [email protected]: Please update to v 2.2.x
#
# > [email protected] postinstall /app/node_modules/esbuild
# > node install.js
#
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN [email protected] No description
# npm WARN [email protected] No repository field.
# npm WARN [email protected] No license field.
#
# added 327 packages from 301 contributors and audited 329 packages in 35.971s
#
# 26 packages are looking for funding
# run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 6dfabf0ee9f8
# ---> 21fd1b065314
# Step 5/9 : COPY . .
# ---> 43243f95bff7
# Step 6/9 : RUN npm run build
# ---> Running in 4d918cf18584
#
# > [email protected] build /app
# > vite build
#
# - Building production bundle...
#
# [write] dist/index.html 0.39kb, brotli: 0.15kb
# [write] dist/_assets/docker-handbook-github.3adb4865.webp 12.32kb
# [write] dist/_assets/index.eabcae90.js 42.56kb, brotli: 15.40kb
# [write] dist/_assets/style.0637ccc5.css 0.16kb, brotli: 0.10kb
# - Building production bundle...
#
# Build completed in 1.71s.
#
# Removing intermediate container 4d918cf18584
# ---> 187fb3e82d0d
# Step 7/9 : EXPOSE 80
# ---> Running in b3aab5cf5975
# Removing intermediate container b3aab5cf5975
# ---> d6fcc058cfda
# Step 8/9 : FROM nginx:stable-alpine
# stable: Pulling from library/nginx
# 6ec7b7d162b2: Already exists
# 43876acb2da3: Pull complete
# 7a79edd1e27b: Pull complete
# eea03077c87e: Pull complete
# eba7631b45c5: Pull complete
# Digest: sha256:2eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image for nginx:stable
# ---> 05f64a802c26
# Step 9/9 : COPY --from=builder /app/dist /usr/share/nginx/html
# ---> 8c6dfc34a10d
# Successfully built 8c6dfc34a10d
# Successfully tagged hello-dock:prod
Sau khi image đã được tạo, bạn có thể chạy một container mới bằng cách thực hiện lệnh sau:
docker container run
--rm
--detach
--name hello-dock-prod
--publish 8080:80
hello-dock:prod
# 224aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953
Ứng dụng đang chạy sẽ có sẵn trên http://127.0.0.1:8080
:
Các bản dựng nhiều giai đoạn có thể rất hữu ích nếu bạn đang xây dựng các ứng dụng lớn với nhiều phụ thuộc. Nếu được cấu hình đúng cách, image được xây dựng theo nhiều giai đoạn có thể rất tối ưu và nhỏ gọn.
Cách loại bỏ các file không cần thiết trong Docker Image
Nếu bạn đã làm việc với git
một thời gian, bạn có thể đã biết về tập tin .gitignore
trong các dự án. Chúng chứa danh sách các tệp và thư mục được loại trừ khỏi kho lưu trữ.
Chà, Docker cũng có một khái niệm tương tự. Các tập tin .dockerignore
chứa một danh sách các tập tin và thư mục được loại trừ khỏi image được xây dựng. Bạn có thể tìm thấy một tập tin .dockerignore
được tạo sẵn trong thư mục hello-dock
.
.git
*Dockerfile*
*docker-compose*
node_modules
Tập tin .dockerignore
được tạo ra khi build. Các tệp và thư mục được đề cập ở đây sẽ bị bỏ qua bởi chỉ dẫn COPY
. Nhưng nếu bạn thực hiện liên kết ràng buộc, tập tin .dockerignore
sẽ không có hiệu lực. Tôi đã tự thêm tập tin .dockerignore
khi cần thiết vào kho lưu trữ dự án.
Ở phần tiếp theo bạn sẽ tìm hiểu các kiến thức cơ bản về Docker Network và các thao tác Docker Network cơ bản:
Bài viết này được dịch từ cuốn sách The Docker Handbook của Farhan Hasin Chowdhury: