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:

Sổ tay Docker: Thao tác Docker Image cơ bản
Trong hướng dẫn này, bạn sẽ tìm hiểu các thao tác Docker Image cơ bản như: tạo image, xem danh sách image, xóa image, tối ưu image, hiểu về các lớp của image, ...

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.jsnpm 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ệnh npm 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ùng root. 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ẫn USER.
  • Chỉ dẫn RUN mkdir -p /home/node/app tạo thư mục app bên trong thư mục home của người dùng node. 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ẫn COPY, ADD, RUNCMD nào ở phía sau nó.
  • Chỉ dẫn COPY sao chép tập tin package.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ẫn RUN thực thi lệnh npm 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ệp package.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ạng exec.
  • Máy chủ phát triển vite theo mặc định chạy trên cổng 3000 và thêm một lệnh EXPOSE 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.

Cách tạo Dockerfile môi trường Development

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.

Cách làm việc với liên kết ràng buộc trong Docker

Để 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.

Cách làm việc với liên kết ràng buộc trong Docker

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! hello-dock@0.0.0 dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@0.0.0 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 image nginx.
  • 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 đến node.

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áp as 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ụng vite.
  • 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 fsevents@2.1.3: Please update to v 2.2.x
#
# > esbuild@0.8.29 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 fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN hello-dock@0.0.0 No description
# npm WARN hello-dock@0.0.0 No repository field.
# npm WARN hello-dock@0.0.0 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
#
# > hello-dock@0.0.0 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ách thực hiện xây dựng Image nhiều giai đoạn trong Docker

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:

Sổ tay Docker: Thao tác Docker Network cơ bản
Bạn sẽ tìm hiểu các kiến thức cơ bản về Docker Network, các thao tác mạng cơ bản như: tạo và xóa mạng, gắn container vào mạng, gỡ container khỏi mạng.

Bài viết này được dịch từ cuốn sách The Docker Handbook của Farhan Hasin Chowdhury:

The Docker Handbook – 2021 Edition
The concept of containerization itself is pretty old. But the emergence of the Docker Engine [https://docs.docker.com/get-started/overview/#docker-engine] in2013 has made it much easier to containerize your applications. According to the Stack Overflow Developer Survey - 2020[https://insights.stackoverflow.com/survey/2020#overview…
DockerDevOpsLập Trình JavaScript
Bài Viết Liên Quan:
Deploy ứng dụng ASP.NET Core bằng Docker
Trung Nguyen 29/03/2021
Deploy ứng dụng ASP.NET Core bằng Docker

Bài viết này sẽ hướng dẫn bạn chi tiết cách deploy ứng dụng ASP.NET Core bằng Docker.

Sổ tay Docker: Cách sử dụng Docker Compose
Trung Nguyen 20/03/2021
Sổ tay Docker: Cách sử dụng Docker Compose

Trong hướng dẫn này, bạn sẽ tìm hiểu các kiến thức cơ bản về Docker Compose, cách chạy và quản lý các dịch vụ sử dụng Docker Compose.

Sổ tay Docker: Cách chạy ứng dụng JavaScript trên nhiều Container
Trung Nguyen 20/03/2021
Sổ tay Docker: Cách chạy ứng dụng JavaScript trên nhiều Container

Trong hướng dẫn này, bạn sẽ học cách triển khai ứng dụng trên nhiều container trong Docker.

Sổ tay Docker: Thao tác Docker Network cơ bản
Trung Nguyen 19/03/2021
Sổ tay Docker: Thao tác Docker Network cơ bản

Bạn sẽ tìm hiểu các kiến thức cơ bản về Docker Network, các thao tác mạng cơ bản như: tạo và xóa mạng, gắn container vào mạng, gỡ container khỏi mạng.