Docker 参考文档

2020 年 06 月 04 日 • 阅读数: 238

Docker 参考文档

230007zokz6v5kl5xolo1l.png

Docker的架构

Docker Machine

Docker Machine 是一种提供管理主机的工具,常规的,会安装 Docker Machine 在你的本地机器上,然后使用它来创建 Docker Engine ,可以将这些 Docker Engine 创建在本地的虚拟机中,或者是远程的云上

Docker Engine

Docker提供了一个开发,打包,运行app的平台通过 Docker Engine 将应用app和底层infrastructure隔离开来

Docker Engine 是一个 client-server application ,它由三个部分组成

  • Docker:进程后台
  • REST API:与进程交互的接口
  • CLI:客户端

常用命令介绍:

操作 命令 示例
查看 docker 版本 docker version docker version
查看 docker 信息 docker info docker info
查看某命令 help 信息 docker help [command] docker help attach
查看 docker help 信息 docker --help docker --help

Docker 底层技术

  • Namespaces:做隔离pid,net,ipc,mnt,uts
  • Control groups:做资源限制
  • Union file systems:container和image的分层

Docker 相关名词

  • image:Docker镜像,一个打包好的应用,还有应用运行的系统、资源、配置文件等
  • container:image的实例,可以这么理解,我们使用类(image)可以alloc出来一个或者多个实例
  • registry:存放image的仓库,如 Docker官方 提供的,或者国内的 阿里云 提供了一些基本的镜像

Docker Image

image 也就是镜像,主要有以下特点:

  • 文件和mata data的集合(root filesystem)
  • 分层的,并且每一层都可以添加改变删除文件,成为一个新的image
  • 不同的image可以共享相同的layer
  • image本身是ready-only的

常用命令介绍

操作 命令 示例
从 container 创建 image docker commit [container] [imageName] docker commit nostalgic_morse ouruser/sinatra:v2
从 Dockerfile 创建 image docker build -t [imageName] [pathToFolder] docker build ouruser/sinatra:v3 .
查看本地所有 image docker images docker images
在 registry 中搜索镜像 docker search [query] docker search ubuntu
从 registry 中获取镜像 docker pull [imageName] docker pull ubuntu:14.04, docker pull training/webapp
给 image 打 tag docker tag [imageId] [imageName] docker tag 5db5f8471261 ouruser/sinatra:devel
把本地 image 上传到 registry docker push [imageName] docker push ouruser/sinatra
删除本地 image docker rmi [image] docker rmi training/sinatra

Docker Container

container 可以理解为通过镜像创建的实例,特点如下:

  • 通过 image 创建
  • image layer 之上建立一个 container layer (可读写)
  • 它和 image 的关系类似于面向对象中的类和实例
  • image 负责app的存储和分发,container 负责app的运行

常用命令介绍

操作 命令 示例
创建 container docker create docker create chenhengjie123/xwalkdriver
创建并运行 container docker run docker run chenhengjie123/xwalkdriver /bin/bash
创建并运行 container 后进入其 bash 控制台 docker run -t -i image /bin/bash docker run -t -i ubuntu /bin/bash
创建并运行 container 并让其在后台运行,且映射端口 docker run -p [port in container]:[port in physical system] -d [image] [command] docker run -p 5000:5000 -d training/webapp python app.py
查看正在运行的所有 container 信息 docker ps docker ps
查看最后创建的 container docker ps -l docker ps -l
查看所有 container ,包括正在运行和已经关闭的 docker ps -a docker ps -a
输出指定 container 的 stdout 信息 docker logs -f [container] docker logs -f nostalgic_morse
获取 container 指定端口映射关系 docker port [container] [port] docker port nostalgic_morse 5000
查看 container 进程列表 docker top [container] docker top nostalgic_morse
查看 container 详细信息 docker inspect [container] docker inspect nostalgic_morse
停止 continer docker stop [container] docker stop nostalgic_morse
强制停止 container docker kill [container] docker kill nostalgic_morse
启动一个已经停止的 container docker start [container] docker start nostalgic_morse
重启 container (若 container 处于关闭状态,则直接启动) docker restart [container] docker restart nostalgic_morse
删除 container docker rm [container] docker rm nostalgic_morse

Docker的镜像

Docker镜像的获取

获取镜像的方式很简单,前面已经提到过了通过 docker pull [imageName] 可以从远程仓库中获取一个镜像,但是默认情况下镜像源使用的Docker官方的源,速度较慢,所以这里我们可以通过修改配置切换到国内的镜像源

Docker国内源说明:

Docker 官方中国区
https://registry.docker-cn.com

网易
http://hub-mirror.c.163.com

中国科技大学
https://docker.mirrors.ustc.edu.cn

阿里云
https://pee6w651.mirror.aliyuncs.com

修改镜像源方法如下:

直接设置 –registry-mirror 参数,仅对当前的命令有效 
docker run hello-world --registry-mirror=https://docker.mirrors.ustc.edu.cn

修改 /etc/default/docker,加入 DOCKER_OPTS=”镜像地址”,可以有多个 
DOCKER_OPTS="--registry-mirror=https://docker.mirrors.ustc.edu.cn"

新版的 Docker 推荐使用 json 配置文件的方式,默认为 /etc/docker/daemon.json
非默认路径需要修改 dockerd 的 –config-file,在该文件中加入如下内容: 
{
"registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]
}
同时需要修改 /lib/systemd/system/docker.service
添加如下内容,使得我们前面的配置文件生效
EnvironmentFile=-/etc/docker/daemon.json
然后加载配置文件,重启服务器
sudo systemctl daemon-reload
sudo service docker restart

修改好镜像源后,我们就可以拉取一个centos的镜像,直接使用如下命令

docker pull centos

成功后会打印如下输出

Using default tag: latest
latest: Pulling from library/centos
256b176beaff: Pull complete
Digest: sha256:6f6d986d425aeabdc3a02cb61c02abb2e78e57357e92417d6d58332856024faf
Status: Downloaded newer image for centos:latest

Docker镜像的构建

构建镜像的方式有两种,前面也已经介绍过了,一是通过一个实例构建,另一个是通过 **Dockerfile ** 创建

通过实例创建

前面我们 pull 了一个centos的镜像,这里我们就先通过该镜像创建一个centos的实例

docker run -it centos

然后在该实例中安装vim

yum install -y vim

退出实例并将它打包成一个镜像 docker commit [container] [imageName]

这里我们需要先获取到container的 NAMES ,可以通过查看全部实例的命令

docker container ls -a

# 输出
CONTAINER ID 		... 		NAMES
8a3c2e0e4695 		...         infallible_chebyshev

获取到实例名称后,通过该名称打包

docker commit infallible_chebyshev luoyangc/centos

# 输出
sha256:906567bba8bafcad722061f514703610fb2fc897fe6fe78f036595caf07a7440

查看实例会发现我们自己打包的实例已经存在了

docker images

# 输出
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
luoyangc/centos       latest              906567bba8ba        17 seconds ago      348MB
centos                latest              5182e96772bf        5 weeks ago         200MB

进一步查看镜像的详情可以发现,我们创建的实例是在原始的centos上再次封装了一层

docker history 5182e96772bf

# 输出
IMAGE            CREATED          CREATED BY                                      SIZE 
5182e96772bf     5 weeks ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>        5 weeks ago      /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B
<missing>        5 weeks ago      /bin/sh -c #(nop) ADD file:6340c690b08865d7e…   200MB

docker history 906567bba8ba

# 输出
IMAGE            CREATED           CREATED BY                                      SIZE 
906567bba8ba     a minute ago     /bin/bash                                       149MB
5182e96772bf     5 weeks ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>        5 weeks ago      /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B
<missing>        5 weeks ago      /bin/sh -c #(nop) ADD file:6340c690b08865d7e…   200MB

通过 **Dockerfile ** 的方式

实际上通过实例的方式创建镜像是比较简单的,但并非最好的选择,它省略了很多细节,别人拿到镜像后,根本不知道我们做了哪些事情,所以,跟推荐使用 **Dockerfile ** 的方式创建实例

我们先创建一个目录 mkdir docker-centps-vim 然后cd到该目录下

然后创建一个 Dockerfile 文件,并写入以下数据

FROM centos
RUN yum install -y vim

通过 build 命令来创建一个镜像

通过下面的输出可以看出来,在安装vim的过程中实际上它也是去运行了一个临时实例,并且在最后它删除了实例

docker build -t luoyangc/centos .

# 输出
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM centos
 ---> 5182e96772bf
Step 2/2 : RUN yum install -y vim
 ---> Running in 7bec0fc23d0c
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
 * base: mirrors.shu.edu.cn
 * extras: mirrors.aliyun.com
 * updates: centos.ustc.edu.cn
Resolving Dependencies
 ...
Complete!                                      
Removing intermediate container 7bec0fc23d0c   
 ---> c1d322361e8c                             
Successfully built c1d322361e8c                
Successfully tagged luoyangc/centos-vim:latest 

Dockerfile的语法

FROM

功能为指定基础镜像,并且必须是第一条指令

如果不以任何镜像为基础,那么写法为:FROM scratch,同时意味着接下来所写的指令将作为镜像的第一层开始

语法:其中 是可选项,如果没有选择,那么默认值为latest

FROM <image>
FROM <image>:<tag>
FROM <image>:<digest> 

**注:**建议使用官方的image作为base image


LABEL

功能是为镜像指定标签

语法:

LABEL <key>=<value> <key>=<value> <key>=<value> ...

一个Dockerfile中可以有多个LABEL,但最好写成一行,如太长需要换行的话则使用\符号

LABEL multi.label1="value1" multi.label2="value2" other="value3"

**注:**LABEL会继承基础镜像种的LABEL,如遇到key相同,则值覆盖


RUN

功能为运行指定的命令

RUN命令有两种格式

1. RUN <command>
2. RUN ["executable", "param1", "param2"]

第一种后边直接跟shell命令

  • 在linux操作系统上默认 /bin/sh -c
  • 在windows操作系统上默认 cmd /S /C

第二种是类似于函数调用

可将executable理解成为可执行文件,后面就是两个参数

两种写法比对:

  • RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME
    
  • RUN ["/bin/bash", "-c", "echo hello"]
    

**注:**多行命令不要写多个RUN,原因是Dockerfile中每一个指令都会建立一层.

多少个RUN就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错


ENV

功能为设置环境变量

语法有两种

1. ENV <key> <value>
2. ENV <key>=<value> ...

两者的区别就是第一种是一次设置一个,第二种是一次设置多个

**注:**尽量使用ENV,它可以增强镜像的可维护性


WORKDIR

设置工作目录,对RUN,CMD,ENTRYPOINT,COPY,ADD生效

语法:

WORKDIR /path/to/workdir

如果不存在则会创建如:

WORKDIR /test
WORKDIR demo
RUN pwd

# pwd执行的结果是/test/demo 

WORKDIR也可以解析环境变量如:

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

# pwd的执行结果是/path/$DIRNAME

**注:**用WORKDIR,不要使用RUN cd,尽量使用绝对路径


ADD

一个复制命令,把文件复制到镜像中

如果把虚拟机与容器想象成两台linux服务器的话,那么这个命令就类似于scp,只是scp需要加用户名和密码的权限验证,而ADD不用

语法如下:

1. ADD <src>... <dest>
2. ADD ["<src>",... "<dest>"]

路径的填写可以是容器内的绝对路径,也可以是相对于工作目录的相对路径

可以是一个本地文件或者是一个本地压缩文件,还可以是一个url

如果把写成一个url,那么ADD就类似于wget命令(但不建议这么做,尽量直接通过 RUN wget <url>

如以下写法都是可以的:

ADD test relativeDir/ 
ADD test /relativeDir
ADD http://example.com/foobar /

**注:**不要把写成一个文件夹,如果是一个文件夹了,复制整个目录的内容,包括文件系统元数据


COPY

看这个名字就知道,又是一个复制命令

语法如下:

1. COPY <src>... <dest>
2. COPY ["<src>",... "<dest>"]

与ADD的区别:COPY的只能是本地文件,其他用法一致

**注:**大部分情况下,COPY优于ADD去使用


VOLUME

可实现挂载功能,可以将内部文件夹或者其他容器中的文件夹挂在到这个容器中

语法为:

VOLUME ["/data"]

说明:

["/data"]可以是一个JsonArray ,也可以是多个值,所以如下几种写法都是正确的

VOLUME ["/var/log/"]
VOLUME /var/log
VOLUME /var/log /var/db

一般的使用场景为需要持久化存储数据时

容器使用的是AUFS,这种文件系统不能持久化数据,当容器关闭后,所有的更改都会丢失

所以当数据需要持久化时用这个命令


EXPOSE

功能为暴漏容器运行时的监听端口给外部

但是EXPOSE并不会使容器访问主机的端口

如果想使得容器与主机的端口有映射关系,必须在容器启动的时候加上 -P参数


CMD

功能为容器启动后默认执行的命令和参数

语法有三种写法

1. CMD ["executable","param1","param2"]
2. CMD ["param1","param2"]
3. CMD command param1 param2

第三种比较好理解了,就是shell这种执行方式和写法

第一种和第二种其实都是可执行文件加上参数的形式

举例说明两种写法:

CMD [ "sh", "-c", "echo $HOME" 
CMD [ "echo", "$HOME" ]

**注:**这里边包括参数的一定要用双引号,就是",不能是单引号,千万不能写成单引号,原因是参数传递后,docker解析的是一个JSON array


ENTRYPOINT

功能是启动时运行的命令

语法如下:

1. ENTRYPOINT ["executable", "param1", "param2"]
2. ENTRYPOINT command param1 param2

第一种就是可执行文件加参数

第二种就是写shell

与CMD比较说明(这俩命令太像了,而且还可以配合使用):

  1. 相同点:
  • 只能写一条,如果写了多条,那么只有最后一条生效
  • 容器启动时才运行,运行时机相同
  1. 不同点:
  • ENTRYPOINT不会被运行的command覆盖,而CMD则会被覆盖
  • 如果我们在Dockerfile种同时写了ENTRYPOINT和CMD,并且CMD指令不是一个完整的可执行命令,那么CMD指定的内容将会作为ENTRYPOINT的参数,如下:
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]
  • 如果我们在Dockerfile种同时写了ENTRYPOINT和CMD,并且CMD是一个完整的指令,那么它们两个会互相覆盖,谁在最后谁生效,如下,那么将执行 ls -altop -b 不会执行:
FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ls -al

**注:**一般使用它让容器以应用程序或者服务的形式运行

Docker镜像的发布

要发布镜像到Docker-Hub中,我们需要先创建一个用户,并且在本地的Docker中登录

注册的方式就不介绍了,登录方式如下

docker login

# 输出
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.

输入用户名和密码
Username: luoyangc
Password:

# 登录成功的输出
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

发布的方式也很简单,命令 docker push [imageName]

docker push luoyangc/hello-word:latest

# 输出
The push refers to repository [docker.io/luoyangc/hello-word]
bd5360427da8: Pushed
latest: digest: sha256:42a8b915c2cba6e801be1e859d729b74fbb39ef1e96d38dbe9434b327c264a56 size: 527

**注:**镜像名称的前缀必须是我们的用户名

Docker的网络

Linux netns

在介绍Docker的网络之前,先介绍一下Linux 的网络命名空间

在专业的网络世界中,经常使用到Virtual Routing and Forwarding(VRF)它可以在单个物理设备上可运行多个虚拟路由,linux中,VRF被叫做 network namespace

查看 network namespace 通过命令 ip netns

[vagrant@docker-node1 ~]$ ip netns help
Usage: ip netns list
       ip netns add NAME
       ip netns set NAME NETNSID
       ip [-all] netns delete [NAME]
       ip netns identify [PID]
       ip netns pids NAME
       ip [-all] netns exec [NAME] cmd ...
       ip netns monitor
       ip netns list-id

首先通过以下命令创建两个 netns

sudo ip netns add test1
sudo ip netns add test2

查看 netns 列表

sudo ip netns list

# 输出
test2
test1

查看 netns 的链路状态,应该注意到的是,在创建了network namespace后,lo interface的状态是down

sudo ip netns exec test1 ip link

# 输出
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

有了 netns 后,还需要创建两个虚拟接口,用来使两个 netns 通信

sudo ip link add veth-a type veth peer name veth-b

通过 ip link 可以查看到刚创建的虚拟接口信息

ip link

# 输出
...
5: veth-b@veth-a: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 12:f4:9b:30:34:b2 brd ff:ff:ff:ff:ff:ff
6: veth-a@veth-b: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 2a:89:c9:5c:76:0f brd ff:ff:ff:ff:ff:ff

然后分别将接口添加到对应的 netns

sudo ip link set veth-a netns test1
sudo ip link set veth-b netns test2

最后分别给 netns 中的虚拟接口分配IP并且激活接口

sudo ip netns exec test1 ip addr add 192.168.0.1/24 dev veth-a
sudo ip netns exec test2 ip addr add 192.168.0.2/24 dev veth-b
sudo ip netns exec test1 ip link set dev vath-a up
sudo ip netns exec test2 ip link set dev vath-b up

验证是否成功可以相互使用 ping 命令进行测试

sudo ip netns exec test1 ping 192.168.0.2

# 输出
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.083 ms
...

docker bridge

前面我们已经介绍了 Linux network namespace ,当Docker的后台服务启动后,会自动的创建一个名为 docker0 的虚拟接口,然后当我们通过Docker创建一个容器的时候,也会自动的创建一个虚拟接口,该接口通过bridge的方式和 docker0 相互连接,并且在创建接口的时候,会自动的分配一个IP地址,效果图如下所示

dockernetwork.PNG

bridge是Docker中的一个默认的网络,除此之外Docker还有一些其他的网络,并且我们也可以自己创建一个网洛,要使用Docker的网络可以通过 docker network 命令,详细命令如下

docker network

# 输出
Usage:  docker network COMMAND

Manage networks

Commands:
  connect     Connect a container to a network              		 # 指定一个网络连接
  create      Create a network										 # 创建一个网络
  disconnect  Disconnect a container from a network					 # 删除一个网络连接
  inspect     Display detailed information on one or more networks   # 查看网络详情
  ls          List networks                                 		 # 查看网络列表
  prune       Remove all unused networks							 # 删除所有网络
  rm          Remove one or more networks                   		 # 删除一个网络

通过 list 可以查看当前Docker中的所以网络

docker network ls

# 输出
NETWORK ID          NAME                DRIVER              SCOPE
2e5bd1d00482        bridge              bridge              local
5d9ed257d1c3        host                host                local
0447ba790cf4        none                null                local

前面我们说过,我们创建的容器都会默认连接到bridge这个网络上,这个时候,就可以直接通过IP地址相互访问,但是很多时候,我们不想直接通过IP访问,而是通过容器名称的方式进行访问,这时候就需要 link 来进行容器间的通信

如下面的代码中,我们创建了一个名为 test1 的容器,然后创建了一个 test2 的容器,并且将 test2 连接到 test1

docker run -d --name test1 centos /bin/sh -c "while true; do sleep 3600; done"

docker run -d --name test2 --link test1 centos /bin/sh -c "while true; do sleep 3600; done"

现在我们在 test2 中就可以直接 ping test1 也可以通信,但在 test1 中不可以反向关联,所以实际上,这中直接 link 的方式使用得并不多,更多的时候,我们直接使用上面提到的 Docker network

前面提到的创建的容器会默认连接到bridge上,但默认的bridge是不支持 link 的,不过我们可以自定义一个网络,这个网络可以支持双向 link

docker network create my_net

docker network connect my_net test1
docker network connect my_net test2

可以通过以下命令查看是否成功,能互相 ping 通,则说明我们的自己创建的网络的 link 生效了

docker exec -it test1 ping test2
docker exec -it test2 ping test1

docker host

前面的bridge网络主要用于Docker容器间通信,如果我们要让我们的容器作为服务器被外部访问的话就需要使用Docker的另一个网络,也就是host

在介绍docker host之前,我们其实也可以简单的通过端口转发的方式,实现外部访问到docker内部服务

这里我们在Docker中起一个Nginx服务器,通过端口映射的方式,将容器的80端口和本地的80端口做映射

docker run --name web -d -p 80:80 nginx

只需要这一步,我们就可以在外部通过虚拟机的IP访问到Docker中的Nginx服务了

我么也可以使用docker host,通过如下命令

docker run -d --name web --network host nginx

通过该命令也可以在外部通过虚拟机的IP访问到Nginx服务,但实现方式是不同的

如果这个时候,我们通过 docker network inspect host 查看host网络的详情可以看到,web容器确实存在该网络中,但是没有IP地址。而如果我们进入到容器内部运行 ip a 命令,可以发现,它的网络和我们虚拟机的网络完全相同,也就是说,它没有使用任何新的接口,而是和主机共享了所有网络接口

**注:**如果使用host的方式,极易发生端口冲突,所以这里还是推荐使用端口映射的方式比较好

docker none

除了前面介绍的两种网络外,Docker还提供了一种 none 的网络,如果指定为该网络,那么容器也不会被分配接口和IP,当然也不会共享主机的接口,因此它不能够被任何外部访问

Docker的数据

Data Volume

有些时候,容器本身会产生一些数据,这些数据如果不进行持久化的处理,就会随着容器的关闭而消失

最典型的就是数据库的容器,我们不希望一旦容器关闭,数据库就被清空,我们希望数据可以保存下来,这时候就可以用到 Data Volume

在前面得 Dockerfile 中也简单得介绍了 VOLUME 这个关键字,它可实现挂载功能,可以将内部文件夹或者其他容器中的文件夹挂在到这个容器中

查看MySQL得 Dockerfile 可以发现这么一段代码

...
VOLUME /var/lib/mysql

由此可以看出,它也是使用了数据持久化的

我们可以起一个MySQL的服务,如下:需要通过 -e 指定一个环境变量作为数据库的密码,这里我们允许为空

docker run -d --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

然后通过 docker valume 可以查看到和 VOLUME 相关的信息

docker volume ls

# 输出
DRIVER              VOLUME NAME
local               29ee55a5a3d189c7d0f2406215cc9afc96158bdd0f1835b3e5575ca8cf6275e5

这个时候,我们删除MySQL的容器的时候,VOLUME 依然存在

但是,现在这种方式并不友好,查看详情可以看到,路径的名称很长

docker volume inspect 29ee55a5a3d189...8cf6275e5

# 输出
[
    {
        "CreatedAt": "2018-09-17T01:32:38Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/29ee55a5a3d189...8cf6275e5/_data",
        "Name": "29ee55a5a3d189c7d0f2406215cc9afc96158bdd0f1835b3e5575ca8cf6275e5",
        "Options": null,
        "Scope": "local"
    }
]

我们可以在启动容器的时候,通过 -v mysql:/var/lib/mysql ,为 VOLUME 起一个别名

docker run -d -v mysql:/var/lib/mysql --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

这个时候通过 docker volume ls 就会看到,VOLUME NAME 为mysql

并且当我们删除这个正在运行的服务之后

docker rm -f mysql1

再重新起一个MySQL的容器,指定它使用的 VOLUME 为mysql的话,这个新的容器就可以继续使用原来的数据了

docker run -d -v mysql:/var/lib/mysql --name mysql2 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

Bind Mouting

我们还可以将容器的文件和本地文件绑定,同步修改,类似于双向数据绑定

使用方式还是通过 -v 参数后面跟上本地目录:容器内部目录,如下面的例子

docker run -d -p 80:80 -v $(pwd):/usr/share/nginx/html --name web luoyangc/nginx

注:$(pwd) 表示当前目录

这种方式更加方便我们去做一些经常修改本地源码的开发场景:

当修改本地的静态资源时,也要相应的修改服务器上对应的资源,这导致了繁琐性

通过docker bind mouting将本地和服务器(容器)上的资源绑定,改变一方都可以使得数据同步,从而达到直接修改本地资源,服务器上的资源自动更新

Docker的集群

Docker Swarm

Swarm是Docker官方提供的一款集群管理工具,其主要作用是把若干台Docker主机抽象为一个整体,并且通过一个入口统一管理这些Docker主机上的各种Docker资源。Swarm和Kubernetes比较类似,但是更加轻,具有的功能也较kubernetes更少一些

Swarm的基本架构如下图所示

swarmdiagram.png

Swarm集群中包含Manager和Worker两类Node,我们可以直接基于Docker Engine来部署任何类型的Node。而且,在Swarm集群运行期间,我们既可以对其作出任何改变,实现对集群的扩容和缩容等,如添加Manager Node,如删除Worker Node,而做这些操作不需要暂停或重启当前的Swarm集群服务

Manager Node负责管理service和调度Task,一个Task表示要在Swarm集群中的某个Node上启动Docker容器,一个或多个Docker容器运行在Swarm集群中的某个Worker Node上。同时,Manager Node还负责编排容器和集群管理功能(或者更准确地说,是具有Manager管理职能的Node),维护集群的状态。需要注意的是,默认情况下,Manager Node也作为一个Worker Node来执行Task,Swarm支持配置Manager只作为一个专用的管理Node

Worker Node接收由Manager Node调度并指派的Task,启动一个Docker容器来运行指定的服务,并且Worker Node需要向Manager Node汇报被指派的Task的执行状态

这其中Service、Task、Container(容器)这个三个概念的关系,如下图

20171215092409429.png

在实际的生产环境中,我们的 Manager 一般都需要2台以上,但下面为了简单演示方便,我们起一台 Manager 两台 worker

首先起一台服务器,并且启动 swarm

--listen-addr 指出的是这个集群暴露给外界调用的HTTPAPI的socket地址

--advertise-addr 指定swarm集群所在的子网络的网卡接口

docker swarm init --listen-addr  192.168.205.10:8888 --advertise-addr  192.168.205.10
Swarm initialized: current node (b093g4r02dhf0ccy7qfzjm8t1) is now a manager.

# 输出
To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-5o37ipkhjlku444wp8nfz4rmteeajkqba3vqhsrhlw33n7noq2-0a45dfyxok0ivdeta1somw0qm 192.168.205.10:8888

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

在输出中,它给了我们一个携带 token 的命令,我们可以通过该命令,将其他服务器加入到 swarm 中,如果不小心忘了这个命令那么可以在manager上运行 docker swarm join-token manager 命令获取

我们再另起两个虚拟机,并且运行上面的命令

docker swarm join --token SWMTKN-1-5o37ipkhjlku444wp8nfz4rmteeajkqba3vqhsrhlw33n7noq2-0a45dfyxok0ivdeta1somw0qm 192.168.205.10: 8888

# 输出,该节点作为一个worker加入了swarm
This node joined a swarm as a worker.

通过这两个命令就已经创建了一个简单的服务器集群

manager 中可以通过 docker node ls 查看当前集群节点

docker node ls

# 输出
ID           HOSTNAME         STATUS    AVAILABILITY   MANAGER STATUS    ENGINE VERSION
b093g... *   swarm-manager    Ready     Active         Leader            18.06.1-ce
qfth2...     swarm-worker1    Ready     Active                           18.06.1-ce
k6sz2...     swarm-worker2    Ready     Active                           18.06.1-ce

这时候我们就可以创建一个service,如下

docker service create --name demo busybox sh -c "while true;do sleep 3600;done"

# 输出
j0q09pp69ovtg74wk99w9ssou
overall progress: 1 out of 1 tasks
1/1: running
verify: Service converged

创建完成以后,manager就会在任意一台服务器上起一个Task来运行容器

可以通过 docker service ls 来查看服务的运行情况

docker service ls

# 输出,REPLICAS可以通过scale来横向扩展,后面介绍
ID               NAME         MODE           REPLICAS        IMAGE               PORTS
j0q09pp69ovt     demo         replicated     1/1             busybox:latest

可以通过 docker service ps service_name 来查看容器的运行情况

docker service ps demo

# 输出,可以看到,容器运行在worker1这台服务器上
ID             NAME     IMAGE          NODE           DESIRED STA   CURRENT STATE       ERROR          PORTS
nrd2ntx9zsnt   demo.1   busybox:lats   warm-worker1   Running     Running 2 minutes ago

在worker1这台服务器上运行 docker ps 就可以查看到容器的运行情况

docker ps

# 输出
CONTAINER ID   IMAGE            COMMAND     CREATED         STATUS    PORTS   NAMES
743895a36c11   busybox:latest   "sh …"      9 minutes ago   up 9m             demo.1...

我们可以通过scale来对服务进行横向扩展

docker service scale demo=5

# 输出,我们启动了5个Task,这些Task都由Manager来调度分配到不同的Node上
demo scaled to 5
overall progress: 5 out of 5 tasks
1/5: running   [==================================================>]
2/5: running   [==================================================>]
3/5: running   [==================================================>]
4/5: running   [==================================================>]
5/5: running   [==================================================>]
verify: Service converged

docker service ls

# 输出,可以看到我们5个Task都启动完成
ID               NAME     MODE            REPLICAS        IMAGE               PORTS
j0q09pp69ovt     demo     replicated      5/5             busybox:latest

docker service ps demo

# 输出,可以看到,worker1和worker2分别运行了2个Task,manager上运行了一个Task
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
nrd2ntx9zsnt        demo.1              busybox:latest      swarm-worker1       Running             Running 19 minutes ago
deewpniqnr9k        demo.2              busybox:latest      swarm-manager       Running             Running 31 seconds ago
4hwekz5gozjx        demo.3              busybox:latest      swarm-worker2       Running             Running 22 seconds ago
nmc44e1ctc0s        demo.4              busybox:latest      swarm-worker2       Running             Running 22 seconds ago
ztkslxx1erow        demo.5              busybox:latest      swarm-worker1       Running             Running 32 seconds ago

Routing Mesh

在介绍集群间的通信的原理之前,先来看个例子

还是上面的一个Swarm构建的Docker集群,在创建服务前我们先创建一个overlay的网络

docker network create -d overlay demo

在manager上我们起一个 whoami 的服务,当我们请求这个服务的时候,它可以返回当前处理请求的主机ID,注意这里需要将服务添加到我们创建的overlay网络中

docker service create --name whoami -d -p 8000:8000 --network demo jwilder/whoami

# 这个时候请求127.0.0.1:8000就可以获取到主机ID
curl 127.0.0.1:8000

# 输出
I'm 068e6e14bbc6

然后,我们将服务横向扩展一下

docker service scale whoami=3

# 可以看到,whoami这个服务已经分发到不同的主机上去了
docker service ps whoami

# 输出
ID                  NAME                IMAGE                   NODE                ...
v6cjt8tgbdht        whoami.1            jwilder/whoami:latest   swarm-manager       ...
ueyzkau728lw        whoami.2            jwilder/whoami:latest   swarm-worker1       ...
f3x4o5s54xly        whoami.3            jwilder/whoami:latest   swarm-worker2       ...

这个时候,我们再次请求 whoami ,会发现输出在发生变化

curl 127.0.0.1:8000
# 输出
I'm 424b38c51f37

curl 127.0.0.1:8000
# 输出
I'm 0f6812ce1014

curl 127.0.0.1:8000
# 输出
I'm 068e6e14bbc6

我们再起一个客户端的服务

docker service create --name client -d --network demo busybox sh -c "while true;do sleep 3600;done"

并且进入命令行中,可以发现,不管ping多少次,返回数据的IP都是同一个

docker exec -it client.1.xxmg1b9orj1z8lrlitydniomv sh

# 通过ping命令访问 whoami 的服务
ping whoami
# 输出
PING whoami (10.0.0.21): 56 data bytes
64 bytes from 10.0.0.21: seq=0 ttl=64 time=0.112 ms
64 bytes from 10.0.0.21: seq=1 ttl=64 time=0.104 ms
...

这时候就出现了一个问题,返回数据的IP都是统一的,但返回数据的主机确不是同一个

实际上Docker Swarm的网络中应用了一个DNS服务,如下图

dns.png

客户端要访问服务端,会先到达一个DNS服务器,该服务器会做一次DNS解析

如果访问的是注册到网络的Service的话,就会转发到对应的IP地址,这个IP地址就是对应的Task的IP地址

如果没找到,则会丢给下一层的DNS服务器

而且,一旦注册到网络中的Service都会自动生成一个VIP(虚拟IP),所有访问Service的数据都会经过它的代理

这就是Docker集群的内部通信过程,至于它底层跨主机的通信是基于VXLan隧道的方式实现的,这里就不过多介绍了,只需要了解这是一个网络层的协议

figure88.png

Ingress Network

介绍了Routing Mesh后,再来说一下,集群外部访问的一个负载均衡的方式

还是先举一个例子,我们先把 whoami 的Task收束到一

docker service scale whoami=1

# 然后这时候可以看到只有一个容器再运行
docker service ps whoami

# 输出
ID                  NAME                IMAGE                   NODE                DESIRED STATE       CURRENT STATE         ERROR               PORTS
v6cjt8tgbdht        whoami.1            jwilder/whoami:latest   swarm-manager       Running             Running 3 hours ago

在外部通过任意IP访问集群的8000端口,可以发现都可以正常访问,这是由于在集群和外网间有一层 Ingress

如下图所示,我们访问没有容器存在的节点时,它会在 内部 帮我们进行一次转发

routingmesh.png

Docker Secret

在 Docker Swarm 服务中,Secret 是一种 BLOB(二进制大对象)数据,就像密码、SSH 私钥、 SSL 证书或那些不应该未加密就直接存储在 Dockerfile 或应用程序代码中的数据

不想在镜像或代码中管理的任何敏感数据,都可以使用 Secret 来管理,它存在Swarm Manager节点 Raftdatabase 里,Secret 可以assign给一个service,这个service就能看到这个 Secret

操作 命令
创建一个 Secret docker secret create SECRET
查看 Secret 列表 docker secret ls
查看 Secret 详情 docker secret inspect SECRET
删除一个或多个 Secret ocker secret rm SECRET [SECRET...]

举例如下

docker secret create mysql-root-pwd password
# 输出
xdd1whjv8zaymd2l2ik18pmsc

docker secret ls
# 输出
ID                          NAME                DRIVER              CREATED             UPDATED
xdd1whjv8zaymd2l2ik18pmsc   mysql-root-pwd                          14 seconds ago      14 seconds ago

docker secret inspect mysql-root-pwd
# 输出
[
    {
        "ID": "xdd1whjv8zaymd2l2ik18pmsc",
        "Version": {
            "Index": 423
        },
        "CreatedAt": "2018-09-21T07:06:10.574099231Z",
        "UpdatedAt": "2018-09-21T07:06:10.574099231Z",
        "Spec": {
            "Name": "mysql-root-pwd",
            "Labels": {}
        }
    }
]

docker secret rm mysql-root-pwd
# 输出
mysql-root-pwd

**注:**这里在创建的时候,使用的是文件的方式创建的,所以最好在创建完成后,将文件删除

创建完 Secret 后,在创建Service的时候通过 --secret 指定要授权的 Secret ,然后就可以在Service的 /run/secrets/ 中找到

Docker的工具

Docker Compose

Docker Compose是一个用来定义和运行复杂应用的Docker工具

一个使用Docker容器的应用,通常由多个容器组成,使用Docker Compose不再需要使用shell脚本来启动容器,Compose 通过一个配置文件来管理多个Docker容器,在配置文件中,所有的容器通过services来定义,然后使用docker-compose脚本来启动,停止和重启应用,和应用中的服务以及所有依赖服务的容器,非常适合组合使用多个容器进行开发的场景

Docker Compose中有三个核心概念,分别是 servicenetworkvolume

一个service代表一个container,这个container可以从dockerhub的image来构建,或者从本地的Dockerfile build出来的image来构建

service的启动类似docker run ,我们可以给其指定network和volume,所以可以给service指定network和volume的引用

先来看一个例子

version: '3'                        # 指定版本号

services:                           # services中定义我们要启动的容器

  wordpress:                        # 定义一个容器的名称
    image: wordpress                # 定义使用的镜像
    ports:                          # 做端口映射,相当于 -p
      - 8080:80
    environment:                    # 定义环境变量,相当于 -v
      WORDPRESS_DB_HOST: mysql
      WORDPRESS_DB_PASSWORD: root
    networks:                       # 定义使用的网络
      - my-bridge                   # 该网络在文件后面有定义

  mysql:                            # 启动另一个容器
    image: mysql                    # 定义使用的镜像 
    environment:                    # 定义环境变量
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress
    volumes:                        # 定义数据映射
      - mysql-data:/var/lib/mysql
    networks:                       # 定义网络
      - my-bridge

volumes:                            # 所有的数据映射
  mysql-data:

networks:                           # 所有的网络定义
  my-bridge:
    driver: bridge

在使用之前我们需要先安装,如果是windows或者macos下都是默认安装好的,但是在Linux上,需要我们手动安装,安装方式如下

sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

# 输出
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   617    0   617    0     0    455      0 --:--:--  0:00:01 --:--:--   455
100 8648k  100 8648k    0     0   207k      0  0:00:41  0:00:41 --:--:--  355k

sudo chmod +x /usr/local/bin/docker-compose

docker-compose --version

# 输出,如果成功输出版本号,表示安装成功
docker-compose version 1.16.1, build 6d1ac21

如果有python的环境并且安装了pip的话,可以直接通过 sudo pip install docker-compose 安装

常用命令如下

操作 命令
构建或重建服务 docker-compose build
命令帮助 docker-compose help
杀掉容器 docker-compose kill
显示容器的输出内容 docker-compose logs
打印绑定的开放端口 docker-compose port
显示容器 docker-compose ps
拉取服务镜像 docker-compose pull
重启服务 docker-compose restart
删除停止的容器 docker-compose rm
运行一个一次性命令 docker-compose run
设置服务的容器数目 docker-compose scale
开启服务 docker-compose start
停止服务 docker-compose stop
创建并启动容器 docker-compose up
停止并删除容器 docker-compose down

Docker Stack

在集群中,不能直接使用docker-compose命令进行部署,但我们如果要部署复杂的应用的话,依然需要使用到docker-compose.yml 配置文件进行部署,不过需要重新改写一些配置

在集群中启动和本地启动,配置文件编写上最大的区别就是 deploy 它是Service的一个子选项用来指定与部署和运行服务相关的配置,deploy 本身的子选项也很多,这里我只简单的说几个常用的

endpoint_mode :指定连接到群组外部客户端服务发现方法

  • endpoint_mode:vip :(默认)Docker 为该服务分配了一个虚拟 IP(VIP)
  • endpoint_mode: dnsrr :DNS轮询(DNSRR)服务发现不使用单个虚拟 IP

mode :指定创建容器的方式

  • global :只创建一个容器
  • replicated :(默认)指定容器数量

placement :指定 constraintspreferences

  • constraints- node.role == manager 指定创建在那个节点之上
  • preferences- spread: node.labels.zone

replicas :如果服务是 replicated 指定运行的Task数量

  • replicas: 6 :如这里指定为6,则会起6个容器

resources :配置资源限制

  • 此例子中,redis 服务限制使用不超过 50M 的内存和 0.50(50%)可用处理时间(CPU)
  • 并且 保留 20M 了内存和 0.25 CPU时间
version: '3'
services:
  redis:
    image: redis:alpine
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
        reservations:
          cpus: '0.25'
          memory: 20M

配置还有许多就不一一介绍了

然后通过 docker stack 命令进行部署

操作 命令
创建或更新一个stack deploy
运行中了所有stack ls
具体stack的task详情 ps
删除stack rm
列出stack的service详情 service

Docker的实践

Docker 打包Flask

我们还是先创建一个目录 mkdir docker-flask-hello

然后创建一个python flask的应用,这里命名为 app.py

在这个应用中,我们只编写了一个简单的web应用,代码如下

from flask import Flask


app = Flask(__name__)


@app.route('/')
def hello():
    return "hello docker"


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000)

要将该应用打包成一个docker的镜像,就需要创建一个 Dockerfile ,代码如下

From python:3.6                                             # 依赖python3.6
LABEL maintainer="Luo Yang<luoyang15261829198@gmail.com>"   # 镜像的说明信息
RUN pip install flask                                       # 安装flask依赖
COPY app.py /app/                                           # 将python代码添加到镜像内
WORKDIR /app                                                # 切换工作目录
EXPOSE 5000                                                 # 暴露5000端口
CMD ["python", "app.py"]                                    # 使用python运行app.py文件

打包镜像

docker build -t luoyang/flask-hello .

运行实例

docker run luoyang/flask-hello

# 输出
 * Serving Flask app "app" (lazy loading)                                 
 * Environment: production                                                
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.                                  
 * Debug mode: off                                                        
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)                 

# 通过-d命令使程序在后台运行
docker run -d luoyang/flask-hello    

Docker 容器通信

这里我们还是使用python3.6,然后还需要下载 redis 的镜像

docker pull python:3.6
docker pull redis

然后我们创建一个文件夹,这里命名为 flask-redis

在创建 flask 的容器前,我们先启动 redis 的服务

docker run -d --name redis redis

**注:**这里我们没有使用端口映射,因为我们的redis服务并不希望被外部访问

然后创建 flask 的应用

from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
    redis.incr('hits')
    return 'Hello Container World! I have been seen %s times and my hostname is %s.\
    n' % (redis.get('hits'),socket.gethostname())


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

**注:**这里我们没有直接使用Redis的IP地址,而是使用了一个从外部获取到的环境变量,这个需要在后面启动容器的时候传递

创建Dockerfile

FROM python:3.6
LABEL maintaner="Luo Yang <luoyang15261829198@gmail.com>"
COPY . /app
WORKDIR /app
RUN pip install flask redis
EXPOSE 5000
CMD [ "python", "app.py" ]

打包镜像

docker build -t luoyang/flask-redis .

启动容器,参数的解释如下:

-d 表示后台运行;--link 和redis服务建立连接;--name 指定容器名称;-p 端口映射;-e 设定环境变量

docker run -d --link redis --name flask-redis -p 5000:5000 -e REDIS_HOST=redis luoyang/flask-redis

服务启动后,我们可以进入到容器中,通过 env 查看当前环境变量

[vagrant@docker-node1 flask-redis]$ docker exec -it flask-redis /bin/bash
root@e356f0de7d1e:/app# env

# 输出
...
REDIS_HOST=redis
REDIS_PORT_6379_TCP_PORT=6379
...

如果发现上面 REDIS_HOST 的环境变量正确的话,基本上我们访问redis的服务也就没有问题了

这个时候,我们就可以在外部通过虚拟机的IP加5000端口访问到 flask 的应用了,并且每次访问该应用都会使得redis中得 hits 的值 +1

Docker Compose

对于上面的Docker多容器的问题,我们可以使用Docker compose进行统一的管理

下面我们依然使用python3.6的基本镜像,然后创建我们自己的一个 Django 的容器,同时加上 Mysql 的服务

先拉取一下基础镜像

docker pull python:3.6
docker pull mysql

然后构建我们自己的镜像

FROM python:3.6

WORKDIR /app

RUN pip install django pymysql

ADD /demo /app

EXPOSE 8000

CMD python manage.py runserver 0.0.0.0:8000

Django 的项目就是一个初始的项目,然后将数据库换成了 MySQL

需要修改一下配置文件,如下

import pymysql
pymysql.install_as_MySQLdb()
connect = os.getenv('MYSQL', '172.168.0.1')

...

DATABASES = {                                 
     'default': {                             
         'ENGINE': 'django.db.backends.mysql',
         'NAME': 'django_demo',               
         'USER': 'root',                      
         'PASSWORD': 'amor77895',             
         'HOST': connect,                     
    }                                         
}                                                                           

然后通过命令将项目打包

docker build -t luoyangc/django-demo .

打包完成后,就可以开始构建 docker-compose.yml 配置文件了

version: '3'                           
                                       
services:                              
                                       
  django:                              
    image: luoyangc/django-demo        
    ports:                             
      - 80:8000                          
    environment:                       
      MYSQL: mysql                     
    networks:                          
      - django-mysql                   
                                       
  mysql:                               
    image: mysql                       
    environment:                       
      MYSQL_ROOT_PASSWORD: amor77895   
      MYSQL_DATABASE: django_demo      
    volumes:                           
      - "mysql-data:/var/lib/mysql"    
    networks:                          
      - django-mysql                   
                                       
volumes:                               
  mysql-data:                          
                                       
networks:                              
  django-mysql:                        

直接使用 docker-compose up -d 即可在后台运行了

Docker 集群部署

还是上面的Django项目,我们将它部署到Docker swarm创建的集群当中

修改配置文件

version: '3'

services:

  django:
    image: luoyangc/django-demo
    ports:
      - 80:8080
    environment:
      MYSQL: mysql
    networks:
      - django-mysql
    deploy:
      mode: replicated
      replicas: 3

  mysql:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: amor77895
      MYSQL_DATABASE: django_demo
    volumes:
      - "mysql-data:/var/lib/mysql"
    networks:
      - django-mysql
    deploy:
      mode: global
      placement:
        constraints:
          - node.role == manager

volumes:
  mysql-data:

networks:
  django-mysql:
    driver: overlay

启动服务

docker stack deploy django-demo --compose-file docker-compose.yml

通过以下命令查看运行情况

docker stack ps django-demo

# 输出
ID                  NAME                                          IMAGE                         NODE                DESIRED STATE       CURRENT STATE                  ERROR               PORTS
w0gr3y3p8nv2        django-demo_mysql.b093g4r02dhf0ccy7qfzjm8t1   mysql:latest                  swarm-manager       Running             Running 56 seconds ago
c039nymhqa6h        django-demo_django.1                          luoyangc/django-demo:latest   swarm-manager       Running             Running about a minute ago
jfsrg330dwba        django-demo_django.2                          luoyangc/django-demo:latest   swarm-worker2       Running             Preparing about a minute ago
p1wv90tztpjg        django-demo_django.3                          luoyangc/django-demo:latest   swarm-worker1       Running             Preparing about a minute ago

**注:**我们自己创建的Image不会同步到其他节点,所以在其他节点需要手动创建或拉取

还可以通过以下命令查看service的运行情况

docker stack services django-demo

# 输出
ID             NAME               MODE        REPLICAS   IMAGE          PORTS
7d8swkxpeab1   django-demo_django replicated  3/3        lu...:latest   *:80->8080/tcp
ofw7974dzhcy   django-demo_mysql  global      1/1        mysql:latest

服务启动后,在外部访问任意节点都可以成功访问了

标签: Docker分布式微服务
添加评论
评论列表
没有更多内容