
Bài viết này sẽ hướng dẫn bạn tạo môi trường phát triển Node.js bằng Docker Compose nhanh chóng, linh hoạt và tái sử dụng cho mọi dự án.
Làm việc với containers trong quá trình phát triển mang lại những lợi ích sau:
- Môi trường nhất quán (consistent): Bạn có thể chọn ngôn ngữ và các dependencies bạn muốn cho dự án mà không phải lo lắng về xung đột hệ thống.
- Môi trường cô lập (isolated): Giúp dễ dàng xử lý sự cố và cho phép các thành viên mới trong nhóm nhanh chóng làm quen với dự án.
- Môi trường di động (portable): Cho phép bạn đóng gói và chia sẻ mã nguồn với người khác.
Bạn sẽ tạo hai containers – một cho ứng dụng Node và một cho cơ sở dữ liệu MongoDB – với Docker Compose. Vì ứng dụng này hoạt động với Node và MongoDB, việc thiết lập của bạn sẽ:
- Đồng bộ hóa mã nguồn ứng dụng trên host với mã nguồn trong container để dễ dàng thực hiện thay đổi trong quá trình phát triển.
- Đảm bảo các thay đổi đối với mã nguồn ứng dụng có hiệu lực mà không cần khởi động lại.
- Tạo một cơ sở dữ liệu được bảo vệ bằng tên người dùng và mật khẩu cho dữ liệu của ứng dụng.
- Lưu trữ dữ liệu này một cách bền bỉ.
Cuối cùng, bạn sẽ có một ứng dụng thông tin cá mập đang hoạt động trên các Docker containers.

Điều kiện tiên quyết
Để làm theo hướng dẫn này, bạn cần:
- Một máy chủ phát triển chạy Ubuntu 18.04, cùng với một người dùng không phải root có quyền sudo và một firewall đang hoạt động.
- Docker đã được cài đặt trên máy chủ của bạn.
- Docker Compose đã được cài đặt trên máy chủ của bạn.
Bước 1: Clone Project và sửa đổi dependencies
Bước đầu tiên là clone (sao chép) mã nguồn của dự án và sửa đổi tệp package.json, tệp này bao gồm các dependencies của dự án. Bạn sẽ thêm nodemon vào devDependencies của dự án, chỉ định rằng bạn sẽ sử dụng nó trong quá trình phát triển. Chạy ứng dụng với nodemon đảm bảo rằng ứng dụng sẽ tự động được khởi động lại bất cứ khi nào bạn thực hiện thay đổi đối với mã nguồn của mình.
Đầu tiên, hãy clone repository nodejs-mongo-mongoose từ tài khoản GitHub của DigitalOcean Community. Repository này bao gồm mã nguồn từ hướng dẫn “How To Integrate MongoDB with Your Node Application”.
Clone repository vào một thư mục có tên node_project:
git clone <https://github.com/do-community/nodejs-mongo-mongoose.git> node_project
Điều hướng đến thư mục node_project:
cd node_project
Mở tệp package.json của dự án:
nano package.json
Bên dưới các dependencies của dự án và phía trên dấu ngoặc nhọn đóng }, hãy tạo một đối tượng devDependencies mới bao gồm nodemon:
~/node_project/package.json
...
"dependencies": {
"ejs": "^2.6.1",
"express": "^4.16.4",
"mongoose": "^5.4.10"
},
"devDependencies": {
"nodemon": "^1.18.10"
}
}
Lưu và đóng tệp khi bạn hoàn tất chỉnh sửa. Nếu bạn đang sử dụng nano, nhấn CTRL+X, sau đó Y, sau đó ENTER. Với mã nguồn dự án đã có và các dependencies đã được sửa đổi, bạn có thể chuyển sang bước refactor (tái cấu trúc) mã nguồn cho một quy trình làm việc bằng container.
Bước 2: Cấu hình ứng dụng để làm việc với Containers
Việc sửa đổi ứng dụng của bạn cho quy trình làm việc bằng container có nghĩa là làm cho mã nguồn của bạn trở nên modular hơn. Containers mang lại tính di động giữa các môi trường, và mã nguồn của bạn nên phản ánh điều đó bằng cách tách rời càng nhiều càng tốt khỏi hệ điều hành cơ bản. Để đạt được điều này, bạn sẽ refactor mã nguồn để sử dụng nhiều hơn thuộc tính process.env của Node. Thuộc tính này trả về một đối tượng chứa thông tin về môi trường người dùng của bạn trong thời gian chạy. Bạn có thể sử dụng đối tượng này trong mã nguồn để gán động các thông tin cấu hình trong thời gian chạy bằng các biến môi trường (environment variables).
Bắt đầu với app.js, điểm khởi đầu chính của ứng dụng. Mở tệp:
nano app.js
Bên trong, bạn sẽ thấy một định nghĩa cho một hằng số port, cũng như một hàm listen sử dụng hằng số này để chỉ định cổng mà ứng dụng sẽ lắng nghe.
~/home/node_project/app.js
...
const port = 8080;
...
app.listen(port, function () {
console.log('Example app listening on port 8080!');
});
Định nghĩa lại hằng số port để cho phép gán động trong thời gian chạy bằng cách sử dụng đối tượng process.env. Thực hiện các thay đổi sau đối với định nghĩa hằng số và hàm listen:
~/home/node_project/app.js
...
const port = process.env.PORT || 8080;
...
app.listen(port, function () {
console.log(`Example app listening on ${port}!`);
});
Định nghĩa hằng số mới của bạn sẽ gán port một cách động bằng cách sử dụng giá trị được truyền vào trong thời gian chạy, hoặc là 8080. Tương tự, bạn đã viết lại hàm listen để sử dụng một template literal, chuỗi này sẽ nội suy giá trị cổng khi lắng nghe kết nối.
Khi bạn đã chỉnh sửa xong, hãy lưu và đóng tệp.
Tiếp theo, bạn sẽ sửa đổi thông tin kết nối cơ sở dữ liệu để xóa mọi thông tin xác thực cấu hình. Mở tệp db.js, tệp này chứa thông tin này:
nano db.js
Hiện tại, tệp này thực hiện các điều sau:
- Import Mongoose, một Object Document Mapper (ODM) mà bạn đang sử dụng để tạo các schema (lược đồ) và model (mô hình) cho dữ liệu ứng dụng của bạn.
- Đặt thông tin xác thực cơ sở dữ liệu dưới dạng các hằng số, bao gồm tên người dùng và mật khẩu.
- Kết nối với cơ sở dữ liệu bằng phương thức
mongoose.connect.
Bước đầu tiên của bạn trong việc sửa đổi tệp sẽ là xác định lại các hằng số bao gồm thông tin nhạy cảm. Hiện tại, các hằng số này trông như thế này:
~/node_project/db.js
...
const MONGO_USERNAME = 'sammy';
const MONGO_PASSWORD = 'your_password';
const MONGO_HOSTNAME = '127.0.0.1';
const MONGO_PORT = '27017';
const MONGO_DB = 'sharkinfo';
...
Thay vì mã hóa cứng (hardcoding) thông tin này, bạn có thể sử dụng đối tượng process.env để lấy các giá trị trong thời gian chạy cho các hằng số này. Sửa đổi khối này để trông như thế này:
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
...
Lưu và đóng tệp khi bạn hoàn tất chỉnh sửa.
Tại thời điểm này, bạn đã sửa đổi db.js để hoạt động với các biến môi trường của ứng dụng, nhưng bạn vẫn cần một cách để truyền các biến này vào ứng dụng của mình. Tạo một tệp .env với các giá trị mà bạn có thể truyền vào ứng dụng của mình trong thời gian chạy.
Mở tệp:
nano .env
Tệp này sẽ bao gồm thông tin mà bạn đã xóa khỏi db.js: tên người dùng và mật khẩu cho cơ sở dữ liệu của ứng dụng, cũng như cài đặt cổng và tên cơ sở dữ liệu. Nhớ cập nhật tên người dùng, mật khẩu và tên cơ sở dữ liệu được liệt kê ở đây bằng thông tin của riêng bạn:
~/node_project/.env
MONGO_USERNAME=sammy
MONGO_PASSWORD=your_password
MONGO_PORT=27017
MONGO_DB=sharkinfo
Lưu ý rằng bạn đã xóa cài đặt host ban đầu xuất hiện trong db.js. Giờ đây, bạn sẽ định nghĩa host của mình ở cấp độ tệp Docker Compose, cùng với các thông tin khác về các service và container của bạn.
Lưu và đóng tệp này khi bạn hoàn thành chỉnh sửa.
Vì tệp .env của bạn chứa thông tin nhạy cảm, bạn sẽ muốn đảm bảo rằng nó được bao gồm trong các tệp .dockerignore và .gitignore của dự án để nó không được sao chép vào hệ thống kiểm soát phiên bản hoặc container của bạn.
Mở tệp .dockerignore của bạn:
nano .dockerignore
Thêm dòng sau vào cuối tệp:
~/node_project/.dockerignore
...
.gitignore
.env
Lưu và đóng tệp khi bạn hoàn thành chỉnh sửa.
Tệp .gitignore trong repository này đã bao gồm .env, nhưng hãy cứ kiểm tra xem nó có ở đó không:
nano .gitignore
~~/node_project/.gitignore
...
.env
...
Tại thời điểm này, bạn đã trích xuất thành công thông tin nhạy cảm từ mã dự án của mình và thực hiện các biện pháp để kiểm soát cách thức và nơi thông tin này được sao chép. Bây giờ bạn có thể thêm vào mã kết nối cơ sở dữ liệu của mình để tối ưu hóa nó cho quy trình làm việc container hóa.
Bước 3: Sửa đổi cài đặt kết nối cơ sở dữ liệu
Bước tiếp theo của bạn là làm cho phương thức kết nối cơ sở dữ liệu của bạn trở nên mạnh mẽ hơn bằng cách thêm mã xử lý các trường hợp ứng dụng của bạn không kết nối được với cơ sở dữ liệu. Việc thêm mức độ độ bền này vào mã nguồn ứng dụng của bạn là một recommended practice khi làm việc với containers bằng Compose.
Mở db.js để chỉnh sửa:
nano db.js
Hãy chú ý đến đoạn mã đã được thêm trước đó, cùng với hằng số url cho URI kết nối của Mongo và phương thức connect của Mongoose:
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;
mongoose.connect(url, {useNewUrlParser: true});
Hiện tại, phương thức connect của bạn chấp nhận một tùy chọn yêu cầu Mongoose sử dụng new URL parser của Mongo. Bạn có thể thêm các tùy chọn vào phương thức này để định nghĩa các tham số cho các lần thử kết nối lại. Hãy làm điều này bằng cách tạo một hằng số options bao gồm thông tin liên quan, ngoài tùy chọn new URL parser. Bên dưới các hằng số Mongo của bạn, hãy thêm định nghĩa sau cho hằng số options:
~/node_project/db.js
...
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
...
Tùy chọn reconnectTries yêu cầu Mongoose tiếp tục cố gắng kết nối vô thời hạn, trong khi reconnectInterval định nghĩa khoảng thời gian giữa các lần thử kết nối bằng mili giây. connectTimeoutMS định nghĩa 10 giây là khoảng thời gian mà Mongo driver sẽ chờ trước khi thất bại trong nỗ lực kết nối.
Bây giờ bạn có thể sử dụng hằng số options mới trong phương thức mongoose.connect để tinh chỉnh cài đặt kết nối Mongoose của bạn. Bạn cũng sẽ thêm một promise để xử lý các lỗi kết nối tiềm ẩn.
Hiện tại, phương thức connect của Mongoose trông như thế này:
~/node_project/db.js
...
mongoose.connect(url, {useNewUrlParser: true});
Xóa phương thức connect hiện có và thay thế nó bằng mã sau, bao gồm hằng số options và một promise:
~/node_project/db.js
...
mongoose.connect(url, options).then( function() {
console.log('MongoDB is connected');
})
.catch( function(err) {
console.log(err);
});
Trong trường hợp kết nối thành công, hàm của bạn sẽ ghi lại một thông báo thích hợp; nếu không, nó sẽ catch và ghi lại lỗi, cho phép bạn xử lý sự cố.
Tệp hoàn chỉnh sẽ trông như thế này:
~/node_project/db.js
const mongoose = require('mongoose');
const {
MONGO_USERNAME,
MONGO_PASSWORD,
MONGO_HOSTNAME,
MONGO_PORT,
MONGO_DB
} = process.env;
const options = {
useNewUrlParser: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
connectTimeoutMS: 10000,
};
const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;
mongoose.connect(url, options).then( function() {
console.log('MongoDB is connected');
})
.catch( function(err) {
console.log(err);
});
Lưu và đóng tệp khi bạn đã hoàn thành chỉnh sửa.
Bây giờ bạn đã thêm khả năng phục hồi (resiliency) vào mã ứng dụng của mình để xử lý các trường hợp ứng dụng của bạn có thể không kết nối được với cơ sở dữ liệu. Với mã này, bạn có thể chuyển sang xác định các dịch vụ của mình với Compose.
Bước 4: Xác định Services với Docker Compose
Với mã của bạn đã được tái cấu trúc, bạn đã sẵn sàng viết tệp docker-compose.yml với các định nghĩa dịch vụ của mình. Một service trong Compose là một container đang chạy và các định nghĩa dịch vụ mà bạn sẽ đưa vào tệp docker-compose.yml của mình chứa thông tin về cách mỗi hình ảnh container sẽ chạy. Công cụ Compose cho phép bạn xác định nhiều dịch vụ để xây dựng các ứng dụng multi-container.
Trước khi xác định các dịch vụ của mình, bạn sẽ thêm một công cụ vào dự án của mình có tên là wait-for để đảm bảo rằng ứng dụng của bạn chỉ cố gắng kết nối với cơ sở dữ liệu sau khi các tác vụ khởi động cơ sở dữ liệu đã hoàn tất. Script này sử dụng netcat để thăm dò xem một host và cổng cụ thể có chấp nhận kết nối TCP hay không. Sử dụng nó cho phép bạn kiểm soát các nỗ lực kết nối của ứng dụng với cơ sở dữ liệu của bạn bằng cách kiểm tra xem cơ sở dữ liệu đã sẵn sàng chấp nhận kết nối hay chưa.
Mặc dù Compose cho phép bạn chỉ định các phụ thuộc giữa các dịch vụ bằng cách sử dụng tùy chọn depends_on, thứ tự này dựa trên việc container có đang chạy hay không thay vì tính sẵn sàng của nó. Sử dụng depends_on sẽ không tối ưu cho thiết lập của bạn vì bạn muốn ứng dụng của mình kết nối chỉ khi các tác vụ khởi động cơ sở dữ liệu, bao gồm thêm người dùng và mật khẩu vào cơ sở dữ liệu xác thực admin, đã hoàn tất.
Mở một tệp có tên là wait-for.sh:
nano wait-for.sh
Nhập mã sau vào tệp để tạo hàm thăm dò:
~/node_project/app/wait-for.sh
#!/bin/sh
# original script: <https://github.com/eficode/wait-for/blob/master/wait-for>
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"
Lưu và đóng tệp khi bạn đã hoàn thành việc thêm mã.
Biến script thành tệp thực thi:
chmod +x wait-for.sh
Tiếp theo, mở tệp docker-compose.yml:
nano docker-compose.yml
Đầu tiên, xác định dịch vụ ứng dụng nodejs bằng cách thêm mã sau vào tệp:
~/node_project/docker-compose.yml
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js
Định nghĩa dịch vụ nodejs bao gồm các tùy chọn sau:
- build: Định nghĩa các tùy chọn cấu hình, bao gồm context và dockerfile, sẽ được áp dụng khi Compose xây dựng hình ảnh ứng dụng. Nếu bạn muốn sử dụng một hình ảnh hiện có từ một registry như Docker Hub, bạn có thể sử dụng chỉ thị image thay thế, với thông tin về tên người dùng, repository và image tag của bạn.
- context: Định nghĩa ngữ cảnh xây dựng cho việc xây dựng hình ảnh — trong trường hợp này, là thư mục dự án hiện tại.
- dockerfile: Chỉ định Dockerfile trong thư mục dự án hiện tại của bạn là tệp mà Compose sẽ sử dụng để xây dựng hình ảnh ứng dụng.
- image, container_name: Áp dụng tên cho hình ảnh và container.
- restart: Định nghĩa restart policy. Mặc định là no, nhưng bạn đã đặt container để khởi động lại trừ khi nó bị dừng.
- env_file: Cho Compose biết rằng bạn muốn thêm các biến môi trường từ một tệp có tên là .env, nằm trong build context của bạn.
- environment: Sử dụng tùy chọn này cho phép bạn thêm các cài đặt kết nối Mongo mà bạn đã xác định trong tệp
.env. - ports: Ánh xạ cổng
80trên host đến cổng 8080 trên container. - volumes: Bạn đang bao gồm hai loại mount ở đây:
- Bind mount: Ánh xạ mã ứng dụng của bạn trên host đến thư mục /home/node/app trên container. Điều này sẽ tạo điều kiện phát triển nhanh chóng, vì bất kỳ thay đổi nào bạn thực hiện đối với mã host của mình sẽ được cập nhật ngay lập tức trong container.
- Named volume: Có tên là node_modules. Khi Docker chạy chỉ thị npm install được liệt kê trong Dockerfile ứng dụng, npm sẽ tạo một thư mục node_modules mới trên container bao gồm các gói cần thiết để chạy ứng dụng. Bind mount bạn vừa tạo sẽ ẩn thư mục node_modules mới được tạo này. Vì node_modules trên host trống, bind sẽ ánh xạ một thư mục trống vào container, ghi đè thư mục node_modules mới và ngăn ứng dụng của bạn khởi động. Named volume node_modules giải quyết vấn đề này bằng cách lưu trữ lâu dài nội dung của thư mục /home/node/app/node_modules và mount nó vào container, ẩn đi bind mount.
- networks: Chỉ định rằng dịch vụ ứng dụng của bạn sẽ tham gia mạng app-network, cái mà bạn sẽ xác định ở cuối tệp.
- command: Tùy chọn này cho phép bạn đặt lệnh nên được thực thi khi Compose chạy hình ảnh. Lưu ý rằng điều này sẽ ghi đè chỉ thị CMD mà bạn đã đặt trong Dockerfile ứng dụng của chúng tôi. Ở đây, bạn đang chạy ứng dụng bằng cách sử dụng script wait-for, cái sẽ thăm dò dịch vụ db trên cổng 27017 để kiểm tra xem dịch vụ cơ sở dữ liệu đã sẵn sàng hay chưa. Khi kiểm tra sẵn sàng thành công, script sẽ thực thi lệnh bạn đã đặt,
/home/node/app/node_modules/.bin/nodemon app.js, để khởi động ứng dụng với nodemon.
Tiếp theo, tạo dịch vụ db bằng cách thêm mã sau bên dưới định nghĩa dịch vụ ứng dụng:
~/node_project/docker-compose.yml
...
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
Một số cài đặt được xác định cho dịch vụ nodejs vẫn giữ nguyên, nhưng bạn cũng đã thực hiện các thay đổi sau đối với các định nghĩa image, environment và volumes:
- image: Để tạo dịch vụ này, Compose sẽ kéo hình ảnh Mongo 4.1.8-xenial từ Docker Hub. Bạn đang pin một phiên bản cụ thể để tránh các xung đột có thể xảy ra trong tương lai khi hình ảnh Mongo thay đổi.
- MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD: Hình ảnh mongo cung cấp các biến môi trường này để bạn có thể sửa đổi việc khởi tạo phiên bản cơ sở dữ liệu của mình. MONGO_INITDB_ROOT_USERNAME và MONGO_INITDB_ROOT_PASSWORD cùng nhau tạo ra một người dùng root trong cơ sở dữ liệu xác thực admin và đảm bảo rằng xác thực được bật khi container khởi động. Bạn đã thiết lập MONGO_INITDB_ROOT_USERNAME và MONGO_INITDB_ROOT_PASSWORD bằng các giá trị từ tệp .env, và chuyển các giá trị này tới dịch vụ db thông qua tùy chọn env_file. Điều này có nghĩa là người dùng ứng dụng sammy của bạn sẽ trở thành một người dùng root trên phiên bản cơ sở dữ liệu (database instance), có quyền truy cập vào tất cả các đặc quyền quản trị và vận hành của vai trò đó. Khi làm việc trong môi trường sản xuất (production), bạn nên tạo một người dùng ứng dụng chuyên biệt với các đặc quyền được giới hạn một cách phù hợp.
Ghi chú: Hãy nhớ rằng các biến này sẽ không có hiệu lực nếu bạn khởi động container khi đã có sẵn một thư mục dữ liệu.
- dbdata:/data/db: Named volume dbdata sẽ lưu trữ lâu dài dữ liệu được lưu trữ trong thư mục dữ liệu mặc định của Mongo, /data/db. Điều này sẽ đảm bảo rằng bạn không mất dữ liệu trong trường hợp bạn dừng hoặc xóa container.
Dịch vụ db cũng được thêm vào mạng app-network với tùy chọn networks.
Là bước cuối cùng, thêm các định nghĩa volume và network vào cuối tệp:
~/node_project/docker-compose.yml
...
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
Mạng bridge do người dùng định nghĩa app-network cho phép giao tiếp giữa các container của bạn vì chúng nằm trên cùng một máy chủ Docker daemon. Điều này hợp lý hóa lưu lượng truy cập và giao tiếp trong ứng dụng, vì nó mở tất cả các cổng giữa các container trên cùng một mạng bridge, trong khi không để lộ bất kỳ cổng nào ra thế giới bên ngoài.
Khóa volumes cấp cao nhất của bạn xác định các volume dbdata và node_modules. Khi Docker tạo volume, nội dung của volume được lưu trữ trong một phần của hệ thống tệp host, /var/lib/docker/volumes/, được quản lý bởi Docker. Nội dung của mỗi volume được lưu trữ trong một thư mục dưới /var/lib/docker/volumes/ và được mount vào bất kỳ container nào sử dụng volume đó. Bằng cách này, dữ liệu thông tin cá mập mà người dùng của bạn sẽ tạo sẽ tồn tại lâu dài trong volume dbdata ngay cả khi bạn xóa và tạo lại container db.
Tệp docker-compose.yml đã hoàn thành sẽ trông như thế này:
~/node_project/docker-compose.yml
version: '3'
services:
nodejs:
build:
context: .
dockerfile: Dockerfile
image: nodejs
container_name: nodejs
restart: unless-stopped
env_file: .env
environment:
- MONGO_USERNAME=$MONGO_USERNAME
- MONGO_PASSWORD=$MONGO_PASSWORD
- MONGO_HOSTNAME=db
- MONGO_PORT=$MONGO_PORT
- MONGO_DB=$MONGO_DB
ports:
- "80:8080"
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
networks:
- app-network
command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js
db:
image: mongo:4.1.8-xenial
container_name: db
restart: unless-stopped
env_file: .env
environment:
- MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME
- MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD
volumes:
- dbdata:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
dbdata:
node_modules:
Lưu và đóng tệp khi bạn đã hoàn thành chỉnh sửa.
Với các định nghĩa dịch vụ của bạn đã có, bạn đã sẵn sàng để khởi động ứng dụng.
Bước 5: Kiểm tra ứng dụng
Với tệp docker-compose.yml của bạn đã có, bạn có thể tạo các dịch vụ của mình bằng lệnh docker-compose up. Bạn cũng có thể kiểm tra xem dữ liệu của mình có tồn tại lâu dài hay không bằng cách dừng và xóa các container của mình bằng docker-compose down.
Đầu tiên, xây dựng các hình ảnh container và tạo các dịch vụ bằng cách chạy docker-compose up với cờ -d, cái sau đó sẽ chạy các container nodejs và db ở chế độ nền:
docker-compose up -d
Đầu ra xác nhận rằng các dịch vụ của bạn đã được tạo:
Output
...
Creating db ... done
Creating nodejs ... done
Bạn cũng có thể nhận được thông tin chi tiết hơn về các quá trình khởi động bằng cách hiển thị đầu ra nhật ký từ các dịch vụ:
docker-compose logs
Nếu mọi thứ đã khởi động đúng cách, đây là đầu ra:
Output
...
nodejs | [nodemon] starting `node app.js`
nodejs | Example app listening on 8080!
nodejs | MongoDB is connected
...
db | 2019-02-22T17:26:27.329+0000 I ACCESS [conn2] Successfully authenticated as principalsammy on admin
Bạn cũng có thể kiểm tra trạng thái của các container của mình với docker-compose ps:
docker-compose ps
Đầu ra cho biết các container của bạn đang chạy:
Output
Name Command State Ports
----------------------------------------------------------------------
db docker-entrypoint.sh mongod Up 27017/tcp
nodejs ./wait-for.sh db:27017 -- ... Up 0.0.0.0:80->8080/tcp
Với các dịch vụ của bạn đang chạy, bạn có thể truy cập http://your_server_ip trong trình duyệt:

Nhấp vào nút Get Shark Info để vào một trang có biểu mẫu nhập liệu nơi bạn có thể gửi tên cá mập và mô tả về đặc điểm chung của cá mập đó:

Trong biểu mẫu, thêm một con cá mập mà bạn chọn. Với mục đích minh họa này, hãy thêm Megalodon Shark vào trường Shark Name và Ancient vào trường Shark Character:

Nhấp vào nút Submit và một trang với thông tin cá mập này sẽ được hiển thị lại cho bạn:

Là bước cuối cùng, hãy kiểm tra xem dữ liệu bạn vừa nhập có tồn tại lâu dài không nếu bạn xóa container cơ sở dữ liệu của mình.
Quay lại terminal của bạn, gõ lệnh sau để dừng và xóa các container và mạng của bạn:
docker-compose down
Lưu ý rằng bạn không bao gồm tùy chọn --volumes; do đó, volume dbdata của bạn không bị xóa.
Đầu ra sau xác nhận rằng các container và mạng của bạn đã bị xóa:
Output
Stopping nodejs ... done
Stopping db ... done
Removing nodejs ... done
Removing db ... done
Removing network node_project_app-network
Tạo lại các container:
docker-compose up -d
Bây giờ hãy quay lại biểu mẫu thông tin cá mập:

Nhập một con cá mập mới mà bạn chọn. Ví dụ này sẽ sử dụng Whale Shark và Large

Khi bạn nhấp vào Submit, bạn sẽ nhận thấy rằng con cá mập mới đã được thêm vào bộ sưu tập cá mập trong cơ sở dữ liệu của bạn mà không làm mất dữ liệu bạn đã nhập trước đó:

Ứng dụng của bạn hiện đang chạy trên các container Docker với khả năng lưu trữ lâu dài dữ liệu và đồng bộ hóa mã được kích hoạt.
Kết luận
Bằng cách làm theo hướng dẫn này, bạn đã tạo một thiết lập phát triển cho ứng dụng Node của mình bằng cách sử dụng các container Docker. Bạn đã làm cho dự án của mình theo module và di động hơn bằng cách trích xuất thông tin nhạy cảm và tách rời trạng thái của ứng dụng khỏi mã ứng dụng của bạn. Bạn cũng đã cấu hình một tệp docker-compose.yml cơ bản mà bạn có thể sửa đổi khi nhu cầu và yêu cầu phát triển của bạn thay đổi.
Khi bạn phát triển, bạn có thể quan tâm đến việc tìm hiểu thêm về thiết kế ứng dụng cho các quy trình làm việc container hóa và Cloud Native.