一、docker 介绍、安装、基本命令
1、介绍:
Docker是一个开放源代码软件项目,在Linux操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。
Docker利用Linux核心中的资源分离机制,例如cgroups,以及Linux核心名字空间(namespaces),来创建独立的容器(containers)。
2、安装:
① 设置仓库:
yum install -y yum-utils device-mapper-persistent-data lvm2
② 选择清华镜像源:
yum-config-manager --add-repo https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
③ 安装 Docker Engine-Community:
yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
④ 启动docker:
systemctl start docker
⑤ 查看 docker 版本:
docker version
3、docker 基本命令:
(1) 镜像操作:
① 搜索镜像:docker search 镜像名
② 拉取镜像:
docker pull 镜像名
③ 列出当前镜像:
docker image ls
④ 导出镜像:
docker image save busybox > docker-busybox.tar.gz
可以加上 -o 指定导出的镜像位置。
⑤ 删除镜像:
docker image rm 镜像名
⑥ 导入镜像:
docker image load -i docker-busybox.tar.gz
(2) 容器操作:
① 启动容器:
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
options 常用命令选项:
-t :打开一个终端
-i:交互式访问
--name:容器名字
--network:指定网络
--rm:容器一停,自动删除
-d:剥离与当前终端的关系;否则会一直占据着终端
-p:端口映射,将容器内服务的端口映射在宿主机的指定端口
示例:
docker run --name web1 -d -p 8888:80 nginx:1.14-alpine
② 查询容器:
docker ps:查询运行中的容器;
docker ps -a:查询所有容器。
③ 停止运行的容器:
docker stop 容器名:关闭运行的容器;
docker kill 容器名:杀死运行的容器。
④ 激活关闭的容器:docker start 容器名。
⑤ 查看容器详细信息:docker inspect 容器名。
⑥ 删除容器:
先关闭容器:docker kill 容器名
再删除容器:docker rm -f 容器名
⑦ 对运行的容器执行指定命令:
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
二、docker 网络
1、docker 四种网络模式:
Bridge contauner :桥接式网络模式
Host(open) container :开放式网络模式
Container(join) container :联合挂载式网络模式,是host网络模式的延伸
None(Close) container :封闭式网络模式
2、bridge 网络模式:
(1) 介绍:
当Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,Docker容器会连接到这个虚拟网桥上,所以有默认地址172.17.0.0/16的地址。
docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx命名。
bridge模式是docker的默认网络模式。
(2) 示例:
docker run --name b1 -it --network bridge --rm busybox:latest
以交互模式启动一个基于 busybox 镜像的最新版本的新容器,容器名为 b1,使用默认的桥接网络,并且当容器停止时自动删除它。
3、host 网络模式:
(1) 介绍:
如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace,容器不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。但是,容器的文件系统、进程列表等还是和宿主机隔离的。
(2) 示例:
docker run --name b2 -it --network host --rm busybox:latest
4、container 模式:
(1) 介绍:
指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和指定的容器共享 IP、端口范围等。
两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。
(2) 示例:
在一个终端,使用bridge网络模式启动容器b1:
docker run --name b1 -it --rm busybox:latest
b1的 ip 为172.17.0.2。
在另一个终端使用Container 网络模式创建容器b2:
docker run --name b2 -it --network container:b1 --rm busybox:latest
b2 的 ip 与 b1 相同。
5、none 模式:
(1) 介绍:
使用none模式,Docker容器拥有自己的Network Namespace,但是,Docker容器没有任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息,只有 lo 网络接口。需要手动为Docker容器添加网卡、配置IP等。
不参与网络通信,适用于进程无须网络通信的场景中。
(2) 示例:
三、docker 存储卷
1、存储卷介绍:
(1) 介绍:
"卷"volume 是容器上的一个或多个"目录",此类目录可与宿主机上的某个目录"绑定(关联)"。
例如宿主机的/data/web目录与容器中的/container/data/web目录绑定关系,容器中的进程向这个目录中写数据时,是直接写在宿主机的目录上的。使得可以在宿主机和容器内共享数据库内容,让容器直接访问宿主机中的内容,也可以宿主机向容器供集内容,两者是同步的。
(2) 作用:
① 容器中进程所生成的数据,都保存在存储卷上,当容器被关闭甚至被删除时,不用担心数据被丢失,实现数据可以脱离容器生命周期而持久。当再次重建容器时,如果让它关联到同一个存储卷上时,再创建容器,虽然不是之前的容器,但是数据还是之前的数据。
② 容器可以不置于启动在那台主机之上,如几台主机后面挂载一个NFS,在各自主机上创建容器,而容关联到宿主机的某个目录上。
(3) 分类:
Bind mount volume(绑定挂载卷):在宿主机上的路径要人工的指定一个特定的路径,在容器中也需要指定一个特定的路径,两个已知的路径建立关联关系。
Docker-managed volume(docker管理卷): 只需要在容器内指定容器的挂载点是什么,而被绑定宿主机下的那个目录,是由容器引擎daemon自行创建一个空的目录,适合临时存储。
2、使用 docker-managed volume:
(1) 创建容器 b1:
docker run --name b1 -it -v /data --rm busybox
创建并启动一个名为 b1 的交互式 busybox 容器,挂载宿主机的 /data 目录到容器中。
(2) 查询存储卷信息:
docker inspect b1
(3) 在宿主机的存储卷目录添加内容:
cd /var/lib/docker/volumes/3a9b76e9a5268eba4733e37c90dd92635823dccd242408110c17b222d32af221/_data
(4) 在容器中查看:
(5) 回到宿主机上查看:
由此可知宿主机和容器上的内容是同步更新的。
3、使用 docker mount volume:
(1) 创建容器 b2:
docker run --name b2 -it -v /data/volumes/b2:/data --rm busybox
以交互模式启动一个名为 b2 的 busybox 容器,其中宿主机的 /data/volumes/b2 目录被挂载到容器的 /data 目录下。如果设置存储卷的目录不存在,会自动创建。
(2) 在宿主机的存储卷上插入内容:
cd /data/volumes/b2/
echo "<h1>Bustbox httpd server</h1>" > index.html
(3) 在容器中查看内容:
cat /data/index.html
(4) 容器删除,新建容器b3,修改容器中存储卷路径,存储内容不会改变:
docker run --name b3 -it -v /data/volumes/b2:/data/web/html --rm busybox
4、volumes-from 基于已有容器的存储器,创建容器:
(1) 创建一个 infracon container:
docker run --name infracon -it -v /data/infracon/volume/:/data/web/html busybox:latest
echo "<h1>Nginx server</h1>" > /data/web/html/index.html
(2) 基于infracon container 的存储器,启动一个 nginx container:
docker run --name nginx --network container:infracon --volumes-from infracon -it --rm busybox:latest
(3) 删除容器时删除卷:
docker kill 容器名
docker rm -v 容器名
四、docker registry
1、基于容器制作镜像:
(1) 格式:
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
(2) 示例1:基于容器创建一个新的镜像
① 先运行一个容器:
docker run --name b1 -it busybox
mkdir -p /date/html
echo "<h1>busybox httpd server</h1>" > /date/html/index.html
② 另起终端在b1容器基础上,制作新镜像:
docker commit -p b1
③ 给新镜像打标签:
docker tag 6bf257378449 bean/httpd:v0.1
④ 基于新的镜像运行一个容器:
docker run --name b2 -it bean/httpd:v0.1
(2) 示例2:基于容器创建新的镜像,并修改执行命令CMD
① 基于容器b1创建新的镜像,并修改命令为执行httpd服务:
docker commit -a "B <bean@bean.com>" -c 'CMD ["/bin/httpd","-f","-h","/date/html"]' -p b1 bean/httpd:v0.2
busybox 中 httpd 语法:
-f:不运行为守护进程,在前台运行 ;-h:指定httpd运行的主目录
② 运行新的镜像:
docker run --name b3 -d bean/httpd:v0.2
2、docker registry:
(1) 介绍:
registry用于保存docker 镜像,包括镜像的层次结构和元数据。
启动容器时,docker daemon会试图从本地获取相关的镜像;本地镜像不存在时,其将从registry中下载该镜像并保存到本地。
拉取镜像时,如果不知道registry仓库地址,默认从Docker Hub搜索拉取镜像。
(2) 拉取镜像的格式:
docker pull <registry>[:<port>]/[<namespace>/]<name>:<tag>
registry:仓库服务器地址:不指定默认是docker hub
port:端口;默认是443,因为是https协议
namespace:名称空间,指是哪个用户的仓库
name:仓库名
tag:标签名;默认是latest版本
(3) 将制作的镜像上传到自己的私有registry仓库中:
① 注册docker hub,创建私有仓库:
② 登录docker仓库:
docker login -u bean445
③ 给镜像打标签:
docker tag busybox:latest bean445/httpd:v0.1
④ 上传镜像:
docker push bean445/httpd:v0.1
3、私有仓库 distribution:
(1) 介绍:
docker提供的开源Registry,只作为存储镜像的仓库。
(2) 安装:
① 拉取镜像:
docker pull registry:2.6.2
② 启动registry 容器:
docker run --name registry -p 5000:5000 -v /data/registry:/var/lib/registry -d registry:2.6.2
③ 指定私有Registry地址:
vim /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --insecure-registry 10.1.1.80:5000
systemctl daemon-reload
systemctl restart docker
(3) 上传镜像:
① 给本地镜像打上标签:
docker tag busybox:latest 10.1.1.80:5000/busybox:v0.1
② 上传镜像至私有仓库:
docker push 10.1.1.80:5000/busybox:v0.1
③ 在私有仓库的服务器上验证是否上传成功:
ls /data/registry/docker/registry/v2/
④ 从私有仓库拉取镜像:
先删除原有的镜像:
docker rmi 10.1.1.80:5000/busybox:v0.1
拉取镜像:
docker pull 10.1.1.80:5000/busybox:v0.1
五、dockerfile
1、介绍:
Dockerfile 是一个构建镜像的文本文件,可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像。
Dockerfile 一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令,# 为 Dockerfile 中的注释。
要使用多个Dockerfile 创建镜像,可以在不同目录编写Dockerfile,然后在Dockerfile 所在的目录下构建新的镜像。Dockerfile 中所包含的需要的内容,如COPY的文件、目录等,都需要在Dockerfile 同级目录下存在。
2、dockerfile指令:
(1) FROM:
① 介绍:
FROM 指令必须是 Dockerfile 中非注释行的第一个指令,用于为镜像文件构建过程指定基础镜像。
基准镜像可以是任何可用镜像文件,默认情况下,docker build会在docker主机上查找指定的镜像文件,若不存在,则会自动从 Docker 的公共库 pull 镜像下来。如果找不到指定的镜像文件,docker build 会返回一个错误信息。
如果FROM语句没有指定镜像标签,则默认使用latest标签。
② 格式:
FROM <repository>[:<tag>] 或 FROM <repository>@<digest>
Description: test image
FROM busybox:latest
(2) MAINTAINER:
① 介绍:
用于让dockerfile制作者提供本人的详细信息。
dockerfile 并不限制MAINTAINER 指令可在出现的位置,但推荐将其放置于FROM指令之后。
② 格式:
MAINTAINER <authtor's detail>
MAINTAINER "Bean <bean@666.com>"
(3) COPY:
① 介绍:
用于从 docker 主机复制新文件或者目录至创建的新镜像指定路径中。
② 格式:
COPY <src>... <dest> 或 COPY ["<src>",... "<dest>"]
如果<src>是目录,则其内部文件或子目录会被递归复制,但<src>目录自身不会被复制 ;如果保留源目录结构(即包含 <src> 目录本身),需要在 <dest> 路径中明确指定。
③ 示例1:copy 文件
● 编写dockerfile文件:
COPY index.html /data/web/html/ #要确保dockerfile 同级路径下有index.html文件
● 在dockerfile同级目录下准备好index.html文件:
vim index.html
<h1>Busybox httpd server</h1>
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.1 ./
在当前目录下,根据 Dockerfile 中的指令,构建一个名为 busyboxhttpd、标签为 v0.1 的 Docker 镜像。
● 基于此镜像运行容器进行验证:
docker run --name web1 --rm busyboxhttpd:v0.1 cat /data/web/html/index.html
④ 示例2:COPY目录
● 编写dockerfile文件:
COPY yum.repos.d /etc/yum.repos.d/
● 在dockerfile同级目录下准备好yum.repos.d 目录:
cp -r /etc/yum.repos.d/ ./
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.2 ./
(4) ADD:
① 介绍:
ADD 指令类似于COPY指令,ADD支持使用TAR文件和URL路径。
② 格式:
ADD <src> .. <dest> 或 ADD ["<src>".. "<dest>"]
如果<src>是一个本地系统上的压缩格式的tar文件,它将被展开为一个目录,其行为类似于"tar-x"命令 ;然而,通过URL获取到的tar文件将不会自动展开。
③ 示例1:COPY URL 的 tar 包:
● 编写dockerfile文件:
ADD http://nginx.org/download/nginx-1.15.8.tar.gz /usr/local/src/
● 使用 build 制作镜像:
docker build -t busyboxhttpd:v0.3 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web1 --rm busyboxhttpd:v0.3 ls /usr/local/src
④ 示例2:COPY 本地的路径的tar包:
● 编写dockerfile文件:
ADD nginx-1.15.8.tar.gz /usr/local/src/
● 在dockerfile同级目录下准备nginx-1.15.8.tar.gz:
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.4 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web1 --rm busyboxhttpd:v0.4 ls /usr/local/src /usr/local/src/nginx-1.15.8
(5) WORKDIR:
① 介绍:
用于为Dockerfile中所有的RUN、CMD、ENTRYPOINT、COPY和ADD指定设定工作目录。
② 格式:
WORKDIR <dirpath>
(6) VOLUME:
① 介绍:
用于在image中创建一个挂载点目录。
② 语法:
VOLUME <mountpoint> 或 VOLUME ["<mountpoint>"]
③ 示例:
● 编写dockerfile文件:
VOLUME /data/mysql
● 使用 build 制作镜像:
docker build -t busyboxhttpd:v0.5 ./
基于此新建镜像运行容器,进行验证:
docker run --name web1 --rm -it busyboxhttpd:v0.5 /bin/sh
(7) EXPOSE:
① 介绍:
用于为容器打开指定要监听的端口以实现与外部通信。
② 语法:
EXPOSE <port>[/ <protocol>] [<port>[/ <protocol>] ....
<protocol>用于指定传输层协议,可为tcp或udp二者之一,默认为TCP协议。EXPOSE指令可一次指定多个端口,例如:EXPOSE 11211/udp 11211/tcp。
③ 示例:
● 编写dockerfile文件:
EXPOSE 80/tcp
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.6 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web1 -P --rm -it busyboxhttpd:v0.6 /bin/httpd -f -h /data/web/html
在 Dockerfile 中使用 EXPOSE 指令,实际上是在声明容器内部的应用程序应该监听哪些端口。然而,EXPOSE 并不直接导致 Docker 在宿主机上实际开放这些端口。
要在启动容器时真正地将容器内的端口映射到宿主机的端口上,需要在运行容器时使用 -P 选项。
如果 busyboxhttpd:v0.6 镜像的 Dockerfile 中有 EXPOSE 指令(比如 EXPOSE 80),Docker 会自动将容器的 80 端口映射到宿主机的一个随机端口。
(8) ENV:
① 介绍:
用于为镜像定义所需的环境变量,并可被Dockerfile文件中位于其后的其它指令(如ENV、ADD、COPY等)所调用。
调用格式为$variable_ name 或 ${variable_ name}
② 格式:
ENV <key> <value> 或 ENV <key>=<value>
第一种格式中,<key>之后的所有内容均会被视作其<value>的组成部分, 因此一次只能设置一个变量。
第二种格式可用一次设置多个变量,每个变量为一个"<key>=<value>"的键值对,如果<value>中包含空格,可以以反斜线(\)进行转义,反斜线也可用于续行。
③ 示例:
● 编写dockerfile文件:
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.7 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web1 -P --rm -it busyboxhttpd:v0.7 ls /usr/local/src /data/web/html
(9) RUN:
① 介绍:
用于在docker build构建过程中运行的程序。
② 语法:
RUN <command> 或
RUN ["<executable>", "<param1>", "<param2>"]
③ 示例:
● 编写dockerfile文件:
RUN cd ./src && tar -xf ${WEB_SERVER_PACKAGE}
● 使用build制作镜像:
docker build -t busyboxhttpd:v0.8 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web1 -P --rm -it busyboxhttpd:v0.8 ls /usr/local/src
(10) CMD:
① 介绍:
类似于RUN指令,CMD指令也可用于运行任何命令或应用程序,不过,二者的运行时间点不同:RUN指令运行于镜像文件构建过程中,而CMD指令运行于基于Dockerfile构建出的新镜像文件启动一个容器时。
CMD指令的首要目的在于为启动的容器指定默认要运行的程序,不过CMD指定的命令其可以被docker run的命令行选项所覆盖。
在Dockerfile中可以存在多个CMD指令,但仅最后一个会生效。
② 语法:
CMD <command> 或
CMD ["<executable>","<param1>","<param2>"] 或
CMD ["<param1>","<param2>"]
③ 示例:
● 编写dockerfile文件:
FROM busybox
LABEL maintainer="Bean <bean@666.com>" app="httpd"
ENV WEB_DOC_ROOT="/data/web/html"
RUN mkdir -p ${WEB_DOC_ROOT} && \
echo "<h1>Busybox httpd server</h1>" > ${WEB_DOC_ROOT}/index.html
CMD /bin/httpd -f -h ${WEB_DOC_ROOT}
● 使用build 制作镜像:
● 基于此新建镜像运行容器,进行验证:
httpd正常运行。
使用CMD定义的命令,在启动容器时,会被run追加的指令覆盖:
被 ls / 覆盖,没有执行httpd服务。
(11) ENTRYPOINT:
① 介绍:
类似CMD指令的功能,用于为容器指定默认运行程序。
与CMD不同的是,由ENTRYPOINT启动的程序不会被docker run命令行指定的参数所覆盖,而且,这些命令行参数会被当作参数传递给ENTRYPOINT指定指定的程序。
② 语法:
ENTR YPOINT <command>
ENTRYPOINT ["<executable>", "<param1>", "<param2>"]
③ 示例:
● 编写dockerfile文件:
ENTRYPOINT /bin/httpd -f -h ${WEB_DOC_ROOT}
● 使用build制作镜像:
docker build -t busyboxhttpd:v1.2 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web2 --rm busyboxhttpd:v1.2 ls /
容器不会执行ls / 这个命令;仍然执行的是ENTRYPOINT中设置的命令
(12) HEALTHCHECK:
① 介绍:
HEALTHCHECK指令告诉Docker如何测试容器以检查它是否仍在工作。
② 语法:
HEALTHCHECK [OPTIONS] CMD command
(通过在容器内运行命令来检查容器运行状况)
● OPTIONS 选项:
--interval=DURATION (default: 30s):每隔多长时间探测一次,默认30秒
-- timeout= DURATION (default: 30s):服务响应超时时长,默认30秒
--start-period= DURATION (default: 0s):服务启动多久后开始探测,默认0秒
--retries=N (default: 3):认为检测失败几次为宕机,默认3次
③ 示例:
● 编写dockerfile文件:
检测web2容器的10080端口,10080端口并未开启,检测结果会失败。
HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/
容器启动后等待3秒开始执行健康检查,通过发送一个 HTTP GET 请求到容器内部的 10080 端口,来判断服务是否健康运行。
● 使用build制作镜像:
docker build -t busyboxhttpd:v1.3 ./
● 基于此新建镜像运行容器,进行验证:
docker run --name web2 --rm -d busyboxhttpd:v1.3
(13) ONBUILD:
① 介绍:
Dockerfile 中的 ONBUILD 指令用于设置一个或多个将来在基于当前镜像构建新镜像时需要执行的操作。
当构建一个带有 ONBUILD 指令的镜像(我们称之为"基础镜像"),这些指令并不会立即执行。但是,当其他 Dockerfile 使用 FROM 指令引用这个基础镜像来构建新的镜像时,ONBUILD 后面跟随的命令将会自动触发执行。
② 语法:
ONBUILD < Instruction>
③ 示例:
● 编写第一个Dockerfile文件,作为第二个Dockerfile文件的基础镜像:
ONBUILD RUN echo "<h1>Busybox httpd server2</h1>" >> /data/web/html/index.html
● 编写第2个Dockerfile文件,FROM 基于第1个Dockerfile:
FROM busyboxhttpd:v2.1
● 基于2个Dockerfile文件新建镜像:
docker build -t busyboxhttpd:v2.1 ./
docker build -t busyboxhttpd:v2.2 ./
● 基于两个新镜像启动容器:
docker run --name web1 --rm busyboxhttpd:v2.1 cat /data/web/html/index.html
docker run --name web2 --rm busyboxhttpd:v2.2 cat /data/web/html/index.html
证明ONBUILD指令,只在第2个Dockerfile文件中生效:
六、docker compose
1、介绍:
Compose是一个用于定义和运行多容器Docker应用程序的工具。可以使用Compose文件来配置应用程序的服务。然后从配置中创建并启动所有服务。
2、docker compose 示例:
(1) compose 准备:
① 创建工作目录:
mkdir composetest
② 创建一个app.py文件:
使用Python编写Web应用,使用Flask框架来创建一个Web服务器,并利用Redis作为数据库来存储页面访问计数。
vim app.py
import time
import redis
from flask import Flask
app = Flask(name)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return 'Hello World! I have been seen {} times.\n'.format(count)
if name == "main":
app.run(host="0.0.0.0", debug=True)
③ 创建requirements.txt:
vim requirements.txt
(2) 创建dockerfile:
构建一个python容器,将容器的默认命令设置为python app.py。
vim dockerfile
FROM python:3.4-alpine
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
(3) compose文件定义服务:
① 创建compose文件:
vim docker-compose.yml
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
② 使用compose构建应用:
启动应用程序:docker compose up
在浏览器验证:
在应用程序的终端界面按 Ctrl + C 可停止服务。