Trong phần trước, bạn đã tìm hiểu các thao tác Docker Network cơ bản như tạo và xóa mạng, cách gắn container vào mạng, cách tách container ra khỏi mạng. 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 đã tìm hiểu đầy đủ về mạng trong Docker, trong phần này, bạn sẽ học cách chứa một dự án đa vùng chứa chính thức. Dự án bạn sẽ làm việc là một dự án notes-api
đơn giản được cung cấp bởi Express.js và PostgreSQL.
Trong dự án này có tổng cộng hai container mà bạn sẽ phải kết nối bằng mạng. Ngoài ra, bạn cũng sẽ tìm hiểu về các khái niệm như biến môi trường và volume (ổ đĩa) được đặt tên. Không cần phải nói nhiều nữa, chúng ta hãy bắt đầu ngay.
Máy chủ cơ sở dữ liệu trong dự án này là một máy chủ PostgreSQL đơn giản và sử dụng image Postgres chính thức .
Theo tài liệu chính thức, để chạy container cho image này, bạn phải cung cấp biến môi trường POSTGRES_PASSWORD
. Ngoài cái này, tôi cũng sẽ cung cấp tên cho cơ sở dữ liệu mặc định bằng cách sử dụng biến môi trường POSTGRES_DB
. PostgreSQL mặc định lắng nghe trên cổng 5432
, vì vậy bạn cũng cần phải xuất bản nó.
Để chạy máy chủ cơ sở dữ liệu, bạn có thể thực hiện lệnh sau:
docker container run \
--detach \
--name=notes-db \
--env POSTGRES_DB=notesdb \
--env POSTGRES_PASSWORD=secret \
--network=notes-api-network \
postgres:12
# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# a7b287d34d96 postgres:12 "docker-entrypoint.s…" About a minute ago Up About a minute 5432/tcp notes-db
Các tùy chọn --env
cho lệnh container run
và container create
có thể được sử dụng để cung cấp các biến môi trường cho container. Như bạn có thể thấy, container cơ sở dữ liệu đã được tạo thành công và hiện đang chạy.
Mặc dù container đang chạy nhưng có một vấn đề nhỏ. Các cơ sở dữ liệu như PostgreSQL, MongoDB và MySQL duy trì dữ liệu của chúng trong một thư mục. PostgreSQL sử dụng thư mục /var/lib/postgresql/data
bên trong container để lưu giữ dữ liệu.
Bây giờ điều gì sẽ xảy ra nếu container bị xóa vì một lý do nào đó? Bạn sẽ mất tất cả dữ liệu của mình. Để giải quyết vấn đề này, có thể sử dụng volume.
Trước đây, bạn đã làm việc với các liên kết ràng buộc (bind mount) và volume ẩn danh. Một volume được đặt tên rất giống với một volume ẩn danh ngoại trừ việc bạn có thể tham chiếu tới một volume được đặt tên bằng cách sử dụng tên của nó.
Volume cũng là một đối tượng logic trong Docker và có thể được điều khiển bằng dòng lệnh. Lệnh volume create
có thể được sử dụng để tạo ra một volume được đặt tên.
Cú pháp chung cho lệnh như sau:
docker volume create <volume name>
Để tạo một volume được đặt tên, notes-db-data
bạn có thể thực hiện lệnh sau:
docker volume create notes-db-data
# notes-db-data
docker volume ls
# DRIVER VOLUME NAME
# local notes-db-data
Volume này hiện đã có thể gắn vào /var/lib/postgresql/data
bên trong container notes-db
. Để làm như vậy, hãy dừng và xóa container notes-db
:
docker container stop notes-db
# notes-db
docker container rm notes-db
# notes-db
Bây giờ, hãy chạy một container mới và chỉ định volume bằng cách sử dụng tùy chọn --volume
hoặc -v
.
docker container run \
--detach \
--volume notes-db-data:/var/lib/postgresql/data \
--name=notes-db \
--env POSTGRES_DB=notesdb \
--env POSTGRES_PASSWORD=secret \
--network=notes-api-network \
postgres:12
# 37755e86d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791
Bây giờ hãy kiểm tra container notes-db
để đảm bảo rằng quá trình gắn kết đã thành công:
docker container inspect --format='{{range .Mounts}} {{ .Name }} {{end}}' notes-db
# notes-db-data
Bây giờ dữ liệu sẽ được lưu trữ an toàn bên trong volume notes-db-data
và có thể được sử dụng lại trong tương lai. Một liên kết ràng buộc cũng có thể được sử dụng thay cho một volume được đặt tên ở đây, nhưng tôi thích một volume được đặt tên trong các trường hợp như vậy.
Để xem log từ một container, bạn có thể sử dụng lệnh container logs
. Cú pháp chung cho lệnh như sau:
docker container logs <container identifier>
Để truy cập nhật ký từ container notes-db
, bạn có thể thực hiện lệnh sau:
docker container logs notes-db
# The files belonging to this database system will be owned by user "postgres".
# This user must also own the server process.
# The database cluster will be initialized with locale "en_US.utf8".
# The default database encoding has accordingly been set to "UTF8".
# The default text search configuration will be set to "english".
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /var/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting default max_connections ... 100
# selecting default shared_buffers ... 128MB
# selecting default time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
# pg_ctl -D /var/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling "trust" authentication for local connections
# You can change this by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting for server to start....2021-01-25 13:39:21.613 UTC [47] LOG: starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:21.621 UTC [47] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:21.675 UTC [48] LOG: database system was shut down at 2021-01-25 13:39:21 UTC
# 2021-01-25 13:39:21.685 UTC [47] LOG: database system is ready to accept connections
# done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG: received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG: aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG: background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG: shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG: database system is shut down
# done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG: starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG: listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG: database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG: database system is ready to accept connections
Rõ ràng là đoạn log ở dòng 57, cơ sở dữ liệu đã hoạt động và sẵn sàng chấp nhận các kết nối từ bên ngoài. Ngoài ra còn có tùy chọn --follow
hoặc -f
cho phép bạn đính kèm console vào đầu ra log để nhận một luồng log liên tục.
Như bạn đã tìm hiểu trong phần trước, các container phải được gắn vào mạng cầu nối do người dùng xác định để giao tiếp với nhau bằng cách sử dụng tên container. Để làm như vậy, hãy tạo một mạng có tên notes-api-network
trong hệ thống của bạn:
docker network create notes-api-network
Bây giờ đính kèm container notes-db
vào mạng này bằng cách thực hiện lệnh sau:
docker network connect notes-api-network notes-db
Chuyển đến thư mục mà bạn đã clone từ repository https://github.com/fhsinchy/docker-handbook-projects. Bên trong đó, vào bên trong thư mục notes-api/api
và tạo Dockerfile
mới. Đặt mã sau vào file mới tạo:
# stage one
FROM node:lts-alpine as builder
# install dependencies for node-gyp
RUN apk add --no-cache python make g++
WORKDIR /app
COPY ./package.json .
RUN npm install --only=prod
# stage two
FROM node:lts-alpine
EXPOSE 3000
ENV NODE_ENV=production
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules
CMD [ "node", "bin/www" ]
Đây là một bản dựng nhiều giai đoạn. Giai đoạn đầu tiên được sử dụng để xây dựng và cài đặt các phụ thuộc bằng cách sử dụng node-gyp
và giai đoạn thứ hai là để chạy ứng dụng. Tôi sẽ đi qua các bước ngắn gọn:
node:lts-alpine
làm image cơ sở và đặt tên tên giai đoạn 1 là builder
.python
, make
và g++
. Công cụ node-gyp
yêu cầu ba gói này để chạy.WORKDIR
là /app
.package.json
vào WORKDIR
và cài đặt tất cả các phần phụ thuộc.node:lts-alpine
làm image cơ sở.NODE_ENV
là production
. Đây là điều quan trọng để API chạy đúng cách.node
, tạo thư mục /home/node/app
và thiết lập nó là WORKDIR
.node_modules
từ giai đoạn builder
. Thư mục này chứa tất cả các phụ thuộc được xây dựng cần thiết để chạy ứng dụng.Để xây dựng một image từ file Dockerfile
này, bạn có thể thực hiện lệnh sau:
docker image build --tag notes-api .
# Sending build context to Docker daemon 37.38kB
# Step 1/14 : FROM node:lts-alpine as builder
# ---> 471e8b4eb0b2
# Step 2/14 : RUN apk add --no-cache python make g++
# ---> Running in 5f20a0ecc04b
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
# (1/21) Installing binutils (2.33.1-r0)
# (2/21) Installing gmp (6.1.2-r1)
# (3/21) Installing isl (0.18-r0)
# (4/21) Installing libgomp (9.3.0-r0)
# (5/21) Installing libatomic (9.3.0-r0)
# (6/21) Installing mpfr4 (4.0.2-r1)
# (7/21) Installing mpc1 (1.1.0-r1)
# (8/21) Installing gcc (9.3.0-r0)
# (9/21) Installing musl-dev (1.1.24-r3)
# (10/21) Installing libc-dev (0.7.2-r0)
# (11/21) Installing g++ (9.3.0-r0)
# (12/21) Installing make (4.2.1-r2)
# (13/21) Installing libbz2 (1.0.8-r1)
# (14/21) Installing expat (2.2.9-r1)
# (15/21) Installing libffi (3.2.1-r6)
# (16/21) Installing gdbm (1.13-r1)
# (17/21) Installing ncurses-terminfo-base (6.1_p20200118-r4)
# (18/21) Installing ncurses-libs (6.1_p20200118-r4)
# (19/21) Installing readline (8.0.1-r0)
# (20/21) Installing sqlite-libs (3.30.1-r2)
# (21/21) Installing python2 (2.7.18-r0)
# Executing busybox-1.31.1-r9.trigger
# OK: 212 MiB in 37 packages
# Removing intermediate container 5f20a0ecc04b
# ---> 637ca797d709
# Step 3/14 : WORKDIR /app
# ---> Running in 846361b57599
# Removing intermediate container 846361b57599
# ---> 3d58a482896e
# Step 4/14 : COPY ./package.json .
# ---> 11b387794039
# Step 5/14 : RUN npm install --only=prod
# ---> Running in 2e27e33f935d
# added 269 packages from 220 contributors and audited 1137 packages in 140.322s
#
# 4 packages are looking for funding
# run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 2e27e33f935d
# ---> eb7cb2cb0b20
# Step 6/14 : FROM node:lts-alpine
# ---> 471e8b4eb0b2
# Step 7/14 : EXPOSE 3000
# ---> Running in 4ea24f871747
# Removing intermediate container 4ea24f871747
# ---> 1f0206f2f050
# Step 8/14 : ENV NODE_ENV=production
# ---> Running in 5d40d6ac3b7e
# Removing intermediate container 5d40d6ac3b7e
# ---> 31f62da17929
# Step 9/14 : USER node
# ---> Running in 0963e1fb19a0
# Removing intermediate container 0963e1fb19a0
# ---> 0f4045152b1c
# Step 10/14 : RUN mkdir -p /home/node/app
# ---> Running in 0ac591b3adbd
# Removing intermediate container 0ac591b3adbd
# ---> 5908373dfc75
# Step 11/14 : WORKDIR /home/node/app
# ---> Running in 55253b62ff57
# Removing intermediate container 55253b62ff57
# ---> 2883cdb7c77a
# Step 12/14 : COPY . .
# ---> 8e60893a7142
# Step 13/14 : COPY --from=builder /app/node_modules /home/node/app/node_modules
# ---> 27a85faa4342
# Step 14/14 : CMD [ "node", "bin/www" ]
# ---> Running in 349c8ca6dd3e
# Removing intermediate container 349c8ca6dd3e
# ---> 9ea100571585
# Successfully built 9ea100571585
# Successfully tagged notes-api:latest
Trước khi bạn chạy một container bằng image này, hãy đảm bảo rằng container cơ sở dữ liệu đang chạy và được đính kèm với notes-api-network
.
docker container inspect notes-db
# [
# {
# ...
# "State": {
# "Status": "running",
# "Running": true,
# "Paused": false,
# "Restarting": false,
# "OOMKilled": false,
# "Dead": false,
# "Pid": 11521,
# "ExitCode": 0,
# "Error": "",
# "StartedAt": "2021-01-26T06:55:44.928510218Z",
# "FinishedAt": "2021-01-25T14:19:31.316854657Z"
# },
# ...
# "Mounts": [
# {
# "Type": "volume",
# "Name": "notes-db-data",
# "Source": "/var/lib/docker/volumes/notes-db-data/_data",
# "Destination": "/var/lib/postgresql/data",
# "Driver": "local",
# "Mode": "z",
# "RW": true,
# "Propagation": ""
# }
# ],
# ...
# "NetworkSettings": {
# ...
# "Networks": {
# "bridge": {
# "IPAMConfig": null,
# "Links": null,
# "Aliases": null,
# "NetworkID": "e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab",
# "EndpointID": "2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371",
# "Gateway": "172.17.0.1",
# "IPAddress": "172.17.0.2",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "02:42:ac:11:00:02",
# "DriverOpts": null
# },
# "notes-api-network": {
# "IPAMConfig": {},
# "Links": null,
# "Aliases": [
# "37755e86d627"
# ],
# "NetworkID": "06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea",
# "EndpointID": "5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c",
# "Gateway": "172.18.0.1",
# "IPAddress": "172.18.0.2",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "02:42:ac:12:00:02",
# "DriverOpts": {}
# }
# }
# }
# }
# ]
Tôi đã rút ngắn đầu ra để dễ xem ở đây. Trên hệ thống của tôi, container notes-db
đang chạy, sử dụng volume notes-db-data
và được gắn vào mạng notes-api-network
.
Khi bạn đã chắc chắn rằng mọi thứ đã sẵn sàng, 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 \
--detach \
--name=notes-api \
--env DB_HOST=notes-db \
--env DB_DATABASE=notesdb \
--env DB_PASSWORD=secret \
--publish=3000:3000 \
--network=notes-api-network \
notes-api
# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919
Bạn sẽ có thể tự mình hiểu được lệnh dài này, vì vậy tôi sẽ đi qua các biến môi trường một cách ngắn gọn.
Ứng dụng notes-api
đòi hỏi ba biến môi trường được thiết lập. Chúng như sau:
DB_HOST
- Đây là máy chủ cơ sở dữ liệu. Giả sử rằng cả máy chủ cơ sở dữ liệu và API đều được gắn vào cùng một mạng cầu nối do người dùng xác định, máy chủ cơ sở dữ liệu có thể được tham chiếu để sử dụng tên container của nó là notes-db
trong trường hợp này.DB_DATABASE
- Cơ sở dữ liệu mà API này sẽ sử dụng. Khi chạy máy chủ cơ sở dữ liệu, chúng tôi đặt tên cơ sở dữ liệu mặc định là notesdb
bằng cách sử dụng biến môi trường POSTGRES_DB
. Chúng tôi sẽ sử dụng nó ở đây.DB_PASSWORD
- Mật khẩu để kết nối với cơ sở dữ liệu. Điều này cũng được thiết lập khi chạy máy chủ cơ sở dữ liệu bằng cách sử dụng biến môi trường POSTGRES_PASSWORD
.Để kiểm tra xem container có chạy đúng hay không, bạn có thể sử dụng lệnh container ls
:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# f9ece420872d notes-api "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 0.0.0.0:3000->3000/tcp notes-api
# 37755e86d627 postgres:12 "docker-entrypoint.s…" 17 hours ago Up 14 minutes 5432/tcp notes-db
Container đang chạy. Bạn có thể truy cập http://127.0.0.1:3000/
để xem API đang hoạt động.
Mặc dù container đang chạy, có một điều cuối cùng mà bạn sẽ phải làm trước khi có thể bắt đầu sử dụng nó. Bạn sẽ phải tạo dữ liệu cần thiết để thiết lập các bảng cơ sở dữ liệu và bạn có thể làm điều đó bằng cách thực hiện lệnh npm run db:migrate
bên trong container.
Bạn đã học về cách thực thi các lệnh trong container đã dừng. Một kịch bản khác là thực thi một lệnh bên trong một container đang chạy.
Để thực hiện điều này, bạn sẽ phải sử dụng lệnhexec
để thực hiện một lệnh tùy chỉnh bên trong một container đang chạy.
Cú pháp chung cho lệnh exec
như sau:
docker container exec <container identifier> <command>
Để thực thi npm run db:migrate
bên trong container notes-api
, bạn có thể thực hiện lệnh sau:
docker container exec notes-api npm run db:migrate
# > notes-api@ db:migrate /home/node/app
# > knex migrate:latest
#
# Using environment: production
# Batch 1 run: 1 migrations
Trong trường hợp bạn muốn chạy một lệnh tương tác bên trong một container đang chạy, bạn sẽ phải sử dụng cờ -it
. Ví dụ: nếu bạn muốn truy cập shell đang chạy bên trong container notes-api
, bạn có thể thực hiện lệnh sau:
docker container exec -it notes-api sh
# / # uname -a
# Linux b5b1367d6b31 5.10.9-201.fc33.x86_64 #1 SMP Wed Jan 20 16:56:23 UTC 2021 x86_64 Linux
Quản lý một dự án nhiều container cùng với mạng và volume có nghĩa là viết rất nhiều lệnh. Để đơn giản hóa quy trình, tôi thường nhờ sự trợ giúp từ các tập lệnh shell đơn giản và Makefile.
Bạn sẽ tìm thấy bốn tập lệnh shell trong thư mục notes-api
. Chúng như sau:
boot.sh
- Được sử dụng để khởi động các container nếu chúng đã tồn tại.build.sh
- Tạo và chạy các container. Nó cũng tạo ra image, volume và mạng nếu cần thiết.destroy.sh
- Loại bỏ tất cả các container, volume và mạng được liên kết với dự án này.stop.sh
- Dừng tất cả các container đang chạy.Ngoài ra còn có một Makefile
mà có bốn mục tiêu được đặt tên start
, stop
, build
và destroy
, mỗi cách gọi các kịch bản shell đề cập ở trên.
Nếu container ở trạng thái đang chạy trong hệ thống của bạn, việc thực thi make stop
sẽ dừng tất cả các container. Việc thực thi make destroy
sẽ dừng các container và xóa mọi thứ. Đảm bảo rằng bạn đang chạy các tập lệnh bên trong thư mục notes-api
:
make destroy
# ./shutdown.sh
# stopping api container --->
# notes-api
# api container stopped --->
# stopping db container --->
# notes-db
# db container stopped --->
# shutdown script finished
# ./destroy.sh
# removing api container --->
# notes-api
# api container removed --->
# removing db container --->
# notes-db
# db container removed --->
# removing db data volume --->
# notes-db-data
# db data volume removed --->
# removing network --->
# notes-api-network
# network removed --->
# destroy script finished
Nếu bạn nhận được lỗi bị từ chối quyền, bạn nên thực thi lệnh chmod +x
trên các tập lệnh:
chmod +x boot.sh build.sh destroy.sh shutdown.sh
Tôi sẽ không giải thích các tập lệnh này vì chúng là các câu lệnh if-else
đơn giản cùng với một số lệnh Docker mà bạn đã thấy nhiều lần. Nếu bạn có một số hiểu biết về Linux shell, bạn cũng có thể hiểu các tập lệnh.
Trong hướng dẫn tiếp theo, bạn sẽ học cách sử dụng Docker Compose để chạy và quản lý các dịch vụ phức tạp.
Bài viết này được dịch từ cuốn sách The Docker Handbook của Farhan Hasin Chowdhury:
Bạn có thể vui lòng tắt trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi duy trì hoạt động của trang web.
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.
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.
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.
Trong hướng dẫn này, bạn sẽ học cách xây dựng ứng dụng JavaScript với Docker. Tạo image cho ứng dụng, tối ưu image, chạy container, ...