到目前为止,已学会单个容器的应用,现在要把MySQL加入到应用栈中。下面的问题会不会出现—MySQL运行在哪里?把它安装在同一容器中还是分开?通常情况下,每个容器应该只做一件事情并把这件事做好。下面是容器分开运行的原因:
- 很多时候,要以不同于数据库的方式扩展API和前端。
- 容器分开可以让你独立的控制版本。
- 在本地你可以使用一个数据库容器,但在生产环境,可能用的是数据库托管服务,你不希望随着你的应用一同迁移数据库。
- 运行多个进程需要进程管理器(容器只启动一个进程),这增加了容器启动和停止的复杂性。
还有其他更多的原因,所以如下图所示,最好将应用和数据库分开运行在不同的容器。
多容器应用
容器网络
是否记得,默认情况下,容器是隔离、独立运行的,不知道同一机器上的其他进程或容器的任何信息。这样的话,如何是一个容器同另一容器通信。答案是网络。如果将两个容器放在同一网络中,彼此之间就可以通信。
动词MySQL
有两种方式将一个容器置于一个网络中:
- 在启动容器时指定一个网络。
- 将一个正在运行的容器连接到一个网络。
在下面的步骤中,会首先创建一个网络,然后在启动MySQL容器时关联到这个网络。
- 创建网络
docker network create todo-app
- 启动MySQL容器,然后连接到上面创建的网络。你也可以定义一些初始化数据库所需的环境变量。想要知道更多的MySQL环境变量信息,查看“MySQL Docker Hub listing”的环境变量章节。
- 如果使用的是Mac或Linux,使用下面的命令启动MySQL。
docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:8.0
- 如果使用的windows,在PowerShell中,使用下面的命令:
docker run -d `
--network todo-app --network-alias mysql `
-v todo-mysql-data:/var/lib/mysql `
-e MYSQL_ROOT_PASSWORD=secret `
-e MYSQL_DATABASE=todos `
mysql:8.0
在上面的命令中,会看到--network-alias
,在后续的章节中,会对此标签有详细说明。
在上面的命令中,你会注意到名为
todo-mysql-alias
的数据卷,此卷被挂载到容器的/var/lib/mysql
,这是MySQL容器存储数据的目录。虽然从来没有执行docker volume create
命令。Docker认识到你想要使用一个具名的卷,然后会自动为你创建一个。
- 为确认数据库已正常启动,连接并确认数据库:
docker exec -it <mysql-container-id> mysql -u root -p
当出现密码输入提示时,录入secret
.在MySQL的shell中,列出所有数据库,确认todo
数据库存在。
mysql> SHOW DATABASES;
上述命令的输出结果类似下面:
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| todos |
+--------------------+
5 rows in set (0.00 sec)
- 退出MySQL的shell,返回到宿主机的shell中。
mysql> exit
连接到MySQL
现在MySQL数据库已启动并运行,就可以使用它。但是,如何使用?如果在同中网络中运行另外一个容器,如何发现另外一容器?记住,每个容器都有自己的ip地址。
为了回答上面的问题并更好的理解容器网络,接下来会使用nicolaka/netshoot容器,该容器附带了很多用于排除、高度网络问题的工具。
- 使用nicolaka/netshoot镜像启动一个新的容器。确保和MySQL连接到了同一网络。
docker run -it --network todo-app nicolaka/netshoot
- 在容器里,你会用到
dig
命令,一个好用的DNS工具。找到mysql
主机的ip地址:
dig mysql
会看到类似下面的输出:
; <<>> DiG 9.18.8 <<>> mysql
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22162
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;mysql. IN A
;; ANSWER SECTION:
mysql. 600 IN A 172.23.0.3
;; Query time: 0 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Tue Jun 01 23:47:24 UTC 2023
;; MSG SIZE rcvd: 44
在“ANSWER SECTION”,你会看到mysql的一条A记录,解析为172.23.0.3,你的输出结果中,IP地址可能会不同。mysql并不是有效的主机名,Docker会将它解析为具有该网络别名的容器的IP地址,在启动该容器时,使用--network-alias
定义的别名。
这意味着你的应用只需要简要的连接到名为mysql
的主机,就可以和数据库通信。
连接应用到MySQL容器
待办应用app(http://t.csdn.cn/N4Gkt)支持一些环境变量设置来指定MySQL连接:
MYSQL_HOST
:MySQL服务器的主机名称。MYSQL_USER
:MySQL连接的用户名。MYSQL_PASSWORD
:MySQL连接的用户名密码。MYSQL_DB
:连接之后使用的数据库名称。
当使用环境变量设置连接时,在开发环境是可以接受的,但在生产环境,非常不推荐这么做。Diogo Monica,Docker的前安全主管,写过一片博客(https://blog.diogomonica.com//2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/)来解释为什么。
一个更安全的机制是使用容器编排框架提供的架构支持。在多数场景中,这些加密信息被以文件的形式挂载在容器内。也许你看到过很多app(包括MySQL镜像、待办app镜像)也支持使用_file
后缀的文件来包含这些环境变量。
举个例子,设置MYSQL_PASSWORD_FILE
变量,会使app使用相关文件中的内容做为连接密码。Docker并不需要做其他任何事情来支持这些环境变量。你的app将知道查找环境变量,获取文件内容。
现在可以启动开发就绪的容器。
- 为上面提到的每个环境变量赋值,同时将容器连接到网络,确认在
getting-started/app
目录下执行下面的命令。- 在Mac或Linux上:
docker run -dp 3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:18-alpine \
sh -c "yarn install && yarn run dev"
- 在Windows上:
docker run -dp 3000:3000 `
-w /app -v "$(pwd):/app" `
--network todo-app `
-e MYSQL_HOST=mysql `
-e MYSQL_USER=root `
-e MYSQL_PASSWORD=secret `
-e MYSQL_DB=todos `
node:18-alpine `
sh -c "yarn install && yarn run dev"
- 如果你查看容器的日志(使用
docker logs -f <container-id>
),将会看到类似下面的信息,表明在使用MySQL数据库。
nodemon src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): *.*
[nodemon] starting `node src/index.js`
Connected to mysql db at host mysql
Listening on port 3000
- 在浏览器中打开待办app,添加一些事项到待办列表中。
- 连接MySQL数据库,证实上面添加的事项已已写入到数据库中。记住,数据库密码是
secret
。
docker exec -it <mysql-container-id> mysql -p todos
在MySQL容器的shell中,执行下面的命令:
mysql> select * from todo_items;
+--------------------------------------+--------------------+-----------+
| id | name | completed |
+--------------------------------------+--------------------+-----------+
| 1946ff08-54e6-4446-8249-ed545a0853e5 | Do things! | 0 |
+--------------------------------------+--------------------+-----------+
你的数据库表看起来会不一样,因为已经有不同的待办。
Docker Compose
Docker Compose是用来定义、分享多容器应用的工具。使用Compose,可以创建一个YAML文件来定义服务,用一个简单的命令,就可以启动或停止所有服务。
使用Compose最大的好处是可以在一个文件中定义应用栈,并将该文件放在你工程的根目录,可以非常容易的让其他人参与到项目中来。其他他只需要克隆你的工程,然后启动。实际上,在GitHub/GitLab上,有很多工程就是这样做的。
安装Docker Compose
如果你在Windows、Mac或Linux已安装了Docker Desktop(http://t.csdn.cn/8oZpt),那么Docker Compose已经安装。Play-with-Docker实例也已经安装了Docker Compose。
Docker引擎的独立安装要求Docker Compose作为单独的软件包安装。
使用repository安装
- 在发行版本说明中找到指导说明,设置repository,不同发行版的设置说明如下:
- Ubuntu
- 更新
apt
包索引并安装包,以允许apt
通过HTTPS使用repository
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
- 添加Docker的官方GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
- 使用下面的命令设置repository
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- CentOS
安装yum-utils
包,设置repository
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
- Debian
- 更新
apt
包索引并安装包,以允许apt
通过HTTPS使用repository
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
- 添加Docker的官方GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
- 使用下面的命令设置repository
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- Fedora
安装dnf-plugins-core
包,设置repository
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
- RHEL
安装yum-utils
包,设置repository
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
- SLES
设置repository
sudo zypper addrepo https://download.docker.com/linux/sles/docker-ce.repo
- 更新包索引,安装最新版本的Docker Compose。
- 对于Ubuntu和Debian,使用下面的命令
sudo apt-get update
sudo apt-get install docker-compose-plugin
- 对于基于RPM的发行版,使用下面的命令
sudo yum update
sudo yum install docker-compose-plugin
- 通过检查版本确认Docker Compose被正确安装
docker compose version
Docker Compose version vN.N.N
上面的v.N.N.N
表示最新的版本号,不同时间点不一样。
更新Docker Compose
使用下面的命令更新Docker Compose
- 对于Ubuntu和Debian,运行
sudo apt-get update
sudo apt-get install docker-compose-plugin
- 对于基于RPM的发行版,运行
sudo yum update
sudo yum install docker-compose-plugin
手动安装
这种安装方式安装,后续升级的话,只能手动升级。建议设置Docker的repository,来方便后续的维护
- 下载和安装Docker Compose的CLI插件,运行:
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
mkdir -p $DOCKER_CONFIG/cli-plugins
curl -SL https://github.com/docker/compose/releases/download/v2.18.1/docker-compose-linux-x86_64 -o $DOCKER_CONFIG/cli-plugins/docker-compose
上面的命令下载最新版本的Docker Compose,然后在$HOME
目录下,为激活的用户安装Docker Compose.
为了安装:
- 为你系统里所有用户安装Docker Compose,使用
/usr/local/lib/docker/cli-plugins
替换~/.docker/cli-plugins
- 不同版本的安装,使用你想用的版本号替换
v2.18.1
- 针对不同的操作系统架构,使用你需要的架构替换
x86_x64
- 将可执行权限授予二进制文件
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
或者,如果是为所有用户安装的Docker Compose,则执行:
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
- 验证安装结果
docker compose version
Docker Compose version v2.18.1
创建Compose文件
- 在
/getting-started/app
的根目录下,创建一个名为docker-compose.yaml
的文件。 - 在compose文件中,我们首先定义要作为应用程序一部分运行的服务或容器列表。
services:
现在,让我们依次迁移服务到这个文件中。
定义应用服务
记住,这些命令,是我们用来定义我们应用容器的。
docker run -dp 3000:3000 \
-w /app -v "$(pwd):/app" \
--network todo-app \
-e MYSQL_HOST=mysql \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=secret \
-e MYSQL_DB=todos \
node:18-alpine \
sh -c "yarn install && yarn run dev"
- 首先,让我们定义服务条目和容器的镜像,可以为服务取任何名称。这个名称会自己变成一个网络的简称,会在定义MySQL服务的时候用到。
services:
app:
image: node:18-alpine
- 通常,你会看到挨着
image
的command
,虽然对顺序没有要求,所以可以随意移动到我们的文件中。
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
- 通过为服务定义
port
迁移-p 3000:3000
这部分命令。这里我们会用短语法,但也有更详细的长语法。
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
- 然后,我们通过定义
working_dir
和volumes
来迁移工作目录-w /app
和卷映射-v "$(pwd):/app"
。卷同样有长短语法。
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
working_dir: /app
volumes:
- ./:/app
- 最后,需要使用
environment
来迁移环境变量
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
定义MySQL服务
现在该定义MySQL服务了。下面是我们创建MySQL容器的命令。
docker run -d \
--network todo-app --network-alias mysql \
-v todo-mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=todos \
mysql:8.0
- 先定义名为
mysql
的新服务,所以自动获取网络简称,继续指定要使用的镜像。
services:
app:
# The app service definition
mysql:
image: mysql:8.0
- 接下来,定义数据卷映射,当我们使用
docker run
运行容器时,命令的卷被自动创建。但使用Compose启动时,这并不会发生。我们需要定义卷在顶层的volumes:
部分,然后在服务配置中指定挂载点。通过简单的提供卷名,默认的配置就被使用了。
services:
app:
# The app service definition
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
volumes:
todo-mysql-data:
- 最后,我们仅需要指定环境变量
services:
app:
# The app service definition
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
到目前为止,我们完成了docker-compose.yaml
文件的定义,如下:
services:
app:
image: node:18-alpine
command: sh -c "yarn install && yarn run dev"
ports:
- 3000:3000
working_dir: /app
volumes:
- ./:/app
environment:
MYSQL_HOST: mysql
MYSQL_USER: root
MYSQL_PASSWORD: secret
MYSQL_DB: todos
mysql:
image: mysql:8.0
volumes:
- todo-mysql-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: todos
volumes:
todo-mysql-data:
运行应用栈(stack)
现在有了docker-compose.yaml
,可以启动它。
- 首先确认没有应用或数据库的其他复本容器在运行,通过
docker ps
检查,通过docker rm -f <ids>
- 使用
docker compose up
启动应用栈,添加-d
标签,让所有服务在后台运行。
docker compose up -d
当运行上面命令时,我们会看类似下面的输出:
Creating network "app_default" with the default driver
Creating volume "app_todo-mysql-data" with default driver
Creating app_app_1 ... done
Creating app_mysql_1 ... done
你会注意到,卷和网络都被自动创建。默认情况下,Docker Compose自动创建一个网络专门用于应用栈,这就是我们为什么不在文件中定义的原因。
- 使用
docker compose logs -f
命令查看日志。会看到每个服务的日志交织在一个文件中。当你想关注与时间相关的问题时,这会非常有用。-f
标签跟踪日志,会实时的展示产生的日志。
如果上面的命令已执行,你会看到类似下面的输出:
mysql_1 | 2023-6-04T01:07:26.083639Z 0 [Note] mysqld: ready for connections.
mysql_1 | Version: '8.0.31' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL)
app_1 | Connected to mysql db at host mysql
app_1 | Listening on port 3000
服务名称已在行首展示,用于协助区分信息。如果想看指定服务的日志,可以将服务的名称添加到看日志命令的后面,如docker compose logs -f app
- 到现在,就可以打开我们的应用,我们使用一个简单的命令做到了这些。
在Docker展板上查看应用栈(stack)
如果查看Docker Desktop的展板,你会看到一个名为app的组,这个名称来自Docker Compose中的项目名,用来将容器聚集在一起。默认情况下,项目名是docker-compose.yaml
文件所在的目录。
点击app旁边的展开按钮,就会看到我们在compose文件中定义的两个容器,容器的名字更具有描述性,格式为<service-name>-<replica-number>
。所以非常容易看出来哪个容器是我们的app,哪个容器是我们的MySQL数据库。
全部停止
准备停止时,仅需要执行docker compose down
,或者在Docker Dashboard上点击整个应用程序的垃圾桶。容器会停止,网络会被删除。
默认情况下,执行
docker compose down
,并不会删除在compose文件中命名的卷。如果想要删除这些卷,需要添加--volumes
标签。
一旦停止,就可以转向另外的项目,执行docker compose up
,做好准备为项目贡献,没有比这更简要的了。