Docker基础 本文共有13611个字,关键词: 导出指定容器: `sudo docker export 7691a814370e > ubuntu.tar` 导入指定容器: `cat ubuntu.tar | sudo docker import - test/ubuntu:v1.0` *注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一 个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器 当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定 标签等元数据信息。 #####docker登录操作: 可以通过执行 docker login 命令来输入用户名、密码和邮箱来完成注册和登录。 注册成功后,本地用户目录的 .dockercfg 中将保存用户的认证信息。 #####从镜像仓库查找镜像 `docker search centos` 在查找的时候通过 -s N 参数可以指定仅显示评价为 N 星以上的镜像 用户无需登录即可通过 docker search 命令来查找官方仓库中的镜像,并利用 docker pull 命令来将它下载到 本地。 可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、星级(表示该镜像的受欢迎程度)、是否官 方创建、是否自动创建。 官方的镜像说明是官方项目组创建和维护的,automated 资源允许用户验证镜像的来 源和内容。 自动创建(Automated Builds)功能对于需要经常升级镜像内程序来说,十分方便。 有时候,用户创建了镜 像,安装了某个软件,如果软件发布新版本则需要手动更新镜像。。 而自动创建允许用户通过 Docker Hub 指定跟踪一个目标网站(目前支持 GitHub 或 BitBucket)上的项目,一 旦项目发生新的提交,则自动执行创建。 要配置自动创建,包括如下的步骤: • 创建并登录 Docker Hub,以及目标网站; • 在目标网站中连接帐户到 Docker Hub; • 在 Docker Hub 中 配置一个自动创建 (https://registry.hub.docker.com/builds/add/) ; • 选取一个目标网站中的项目(需要含 Dockerfile)和分支; • 指定 Dockerfile 的位置,并提交创建。 之后,可以 在Docker Hub 的自动创建页面中跟踪每次创建的状态。 在安装了 Docker 后,可以通过获取官方 registry 镜像来运行。 `sudo docker run -d -p 5000:5000 registry` 这将使用官方的 registry 镜像来启动本地的私有仓库。 用户可以通过指定参数来配置私有仓库位置,例如配置镜 像存储到 Amazon S3 服务。 ``` $ sudo docker run \ -e SETTINGS_FLAVOR=s3 \ -e AWS_BUCKET=acme-docker \ -e STORAGE_PATH=/registry \ -e AWS_KEY=xxx \ -e AWS_SECRET=xxx \ -e SEARCH_BACKEND=sqlalchemy \ -p 5000:5000 \ registry ``` 还可以指定本地路径(如 `/home/user/registry-conf` )下的配置文件。 `sudo docker run -d -p 5000:5000 -v /home/user/registry-conf:/registry-conf -e DOCKER_REGIS TRY_CONFIG=/registry-conf/config.yml registry` 如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器。 数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。 首先,创建一个命名的数据卷容器 dbdata: ` sudo docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres` 然后,在其他容器中使用 --volumes-from 来挂载 dbdata 容器中的数据卷。 `sudo docker run -d --volumes-from dbdata --name db1 training/postgres` `sudo docker run -d --volumes-from dbdata --name db2 training/postgres` 还可以使用多个 --volumes-from 参数来从多个容器挂载多个数据卷。 也可以从其他已经挂载了数据卷的容器 来挂载数据卷。 `sudo docker run -d --name db3 --volumes-from db1 training/postgres` 注意:使用 --volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。 如果删除了挂载的容器(包括 dbdata、db1 和 db2),数据卷并不会被自动删除。如果要删除一个数据卷,必 须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。 这可以让用户在容 器之间升级和移动数据卷。 容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P 或 -p 参数来指定端口映射。 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口 -p(小写的)则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的格式有 ``` port:containerPort | ip::containerPort | hostPort:containerPort ``` 映射所有接口地址 hostPort:containerPort 映射到指定地址的指定端口 ip:hostPort:containerPort 映射到指定地址的任意端口 ip::containerPort 还可以使用 udp 标记来指定 udp 端口 `sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py` docker run 的时候如果添加 --rm 标记,则容器在终止后会立刻删除。注意, --rm 和 -d 参数不 能同时使用。 容器互联 使用 --link 参数可以让容器之间安全的进行交互。 先创建一个新的数据库容器 `sudo docker run -d --name db training/postgres` `sudo docker run -d -P --name web --link db:db training/webapp python app.py` 此时,db 容器和 web 容器建立互联关系。 --link 参数的格式为 --link name:alias ,其中 name 是要链接的容器的名称, alias 是这个连接的别名。 Docker 通过 2 种方式为容器公开连接信息: • 环境变量 • 更新 /etc/hosts 文件 使用 env 命令来查看 web 容器的环境变量 使用 env 命令来查看 web 容器的环境变量 `sudo docker run --rm --name web2 --link db:db training/webapp env ` DB_NAME=/web2/db DB_PORT=tcp://172.17.0.5:5432 DB_PORT_5000_TCP=tcp://172.17.0.5:5432 DB_PORT_5000_TCP_PROTO=tcp DB_PORT_5000_TCP_PORT=5432 DB_PORT_5000_TCP_ADDR=172.17.0.5 其中 DB_ 开头的环境变量是供 web 容器连接 db 容器使用,前缀采用大写的连接别名 除了环境变量,Docker 还添加 host 信息到父容器的 /etc/hosts 的文件 #####docker网络: 当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解 为一个软件交换机。它会在挂载到它的网口之间进行转发。 同时,Docker 随机分配一个本地未占用的私有网段(在 RFC1918 (http://tools.ietf.org/html/rfc1918) 中定 义)中的一个地址给 docker0 接口。比如典型的 172.17.42.1 ,掩码为 255.255.0.0 。此后启动的容器内的网 口也会自动分配一个同一网段( 172.17.0.0/16 )的地址。 当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接 口也可以收到相同的数据包)。这对接口一端在容器内,即 eth0 ;另一端在本地并被挂载到 docker0 网 桥,名称以 veth 开头(例如 vethAQI2QT )。通过这种方式,主机可以跟容器通信,容器之间也可以相互通 信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。 其中有些命令选项只有在 Docker 服务启动的时候才能配置,而且不能马上生效。 • -b BRIDGE or --bridge=BRIDGE --指定容器挂载的网桥 • --bip=CIDR --定制 docker0 的掩码 • -H SOCKET... or --host=SOCKET... --Docker 服务端接收命令的通道 • --icc=true|false --是否支持容器之间进行通信 • --ip-forward=true|false --请看下文容器之间的通信 • --iptables=true|false --禁止 Docker 添加 iptables 规则 • --mtu=BYTES --容器网络中的 MTU • --dns=IP_ADDRESS... --使用指定的DNS服务器 • --dns-search=DOMAIN... --指定DNS搜索域 以下只在 docker run 执行时使用,因为它是针对容器的特性内容。 • -h HOSTNAME or --hostname=HOSTNAME --配置容器主机名 • --link=CONTAINER_NAME:ALIAS --添加到另一个容器的连接 • --net=bridge|none|container:NAME_or_ID|host --配置容器的桥接模式 • -p SPEC or --publish=SPEC --映射容器端口到宿主主机 • -P or --publish-all=true|false --映射容器所有端口到宿主主机 docker DNS相关 Docker 没有为每个容器专门定制镜像,那么怎么自定义配置容器的主机名和 DNS 配置呢? 秘诀就是它利用虚拟文件来挂载到来容器的 3 个相关配置文件。 在容器中使用 mount 命令可以看到挂载信息: $ mount ... /dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... /dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... tmpfs on /etc/resolv.conf type tmpfs ... ... 这种机制可以让宿主主机 DNS 信息发生更新后,所有 Docker 容器的 dns 配置通过/etcresolve.conf文件立刻得到更新。 -h HOSTNAME or --hostname=HOSTNAME 设定容器的主机名,它会被写到容器内的/etc/hostname和/etc/hosts --link=CONTAINER_NAME:ALIAS 选项会在创建容器的时候,添加一个其他容器的主机名到/etc/hosts文件中,让新容器的进程可以使用主机名ALIAS 就可以连接它。 --dns=IP_ADDRESS 添加 DNS 服务器到容器的/etc/resolv.conf中,让容器用这个服务器来解析所有不在/etc/hosts中的主机名。 --dns-search=DOMAIN 设定容器的搜索域,当设定搜索域为example.com时,在搜索一个名为 host 的主机时,DNS 不仅搜索host,还会搜索host.example.com。 注意:如果没有上述最后 2 个选项,Docker 会默认用主机上的/etc/resolv.com来配置容器。 #####容器的访问控制: 容器的访问控制,主要通过 Linux 上的 iptables 防火墙来进行管理和实现。 iptables 是 Linux 上默认的防火 墙软件,在大部分发行版中都自带。 #####容器访问外部网络 容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。 $sysctl net.ipv4.ip_forward = 1 如果为 0,说明没有开启转发,则需要手动打开。 $sysctl -w net.ipv4.ip_forward=1 如果在启动 Docker 服务的时候设定 , Docker 就会自动设定系统的 参数为 1。 #####容器之间访问: 容器之间相互访问,需要两方面的支持。 • 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 网桥上。 • 本地系统的防火墙软件 --iptables是否允许通过。 当启动 Docker 服务时候,默认会添加一条转发策略到 iptables 的 FORWARD 链上。策略为通过(ACCEPT),还是禁止(DROP)取决于配置--icc=true(缺省值)还是--icc=false,如果是手动指定了-cc=false则不会添加iptables规则。 默认情况下,不同的容器之间是允许网络互通的,如果为了安全考虑,可以在/etc/default/docker文件中配置DOCKER_OPTS=--icc=false来禁止加入防火墙规则。 用户有时候需要两个容器之间可以直连通信,而不用通过主机网桥进行桥接。 解决办法很简单:创建一对 peer 接口,分别放到两个容器中,配置成点到点链路类型即可。 首先启动 2 个容器: $ sudo docker run -i -t --rm --net=none base /bin/bash root@1f1f4c1f931a:/# $ sudo docker run -i -t --rm --net=none base /bin/bash root@12e343489d2f:/# 找到进程号,然后创建网络名字空间的跟踪文件。 $ sudo docker inspect -f '{{.State.Pid}}' 1f1f4c1f931a 2989 $ sudo docker inspect -f '{{.State.Pid}}' 12e343489d2f 3004 $ sudo mkdir -p /var/run/netns $ sudo ln -s /proc/2989/ns/net /var/run/netns/2989 $ sudo ln -s /proc/3004/ns/net /var/run/netns/3004 创建一对 peer 接口,然后配置路由 $ sudo ip link add A type veth peer name B $ sudo ip link set A netns 2989 $ sudo ip netns exec 2989 ip addr add 10.1.1.1/32 dev A $ sudo ip netns exec 2989 ip link set A up $ sudo ip netns exec 2989 ip route add 10.1.1.2/32 dev A $ sudo ip link set B netns 3004 $ sudo ip netns exec 3004 ip addr add 10.1.1.2/32 dev B $ sudo ip netns exec 3004 ip link set B up $ sudo ip netns exec 3004 ip route add 10.1.1.1/32 dev B 现在这 2 个容器就可以相互 ping 通,并成功建立连接。 点到点链路不需要子网和子网掩码。 此外,也可以指定 --net=none 来创建点到点链路。这样容器还可以通过原先的网络来通信 修改 /etc/default/docker 文件,添加最后一行内容 DOCKER_OPTS="-b=br0" 在启动 Docker 的时候 使用 -b 参数 将容器绑定到物理网络上。重启 Docker 服务后,再进入容器可以看到它已 经绑定到你的物理网络上了 ####Dockerfile #####CMD 支持三种格式 • CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式; • CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用; • CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数; 指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会 被执行。 #####EXPOSE 格式为 EXPOSE [...] 。 告诉 Docker 服务端容器暴露的端口号,供互联系统使用。在启动容器时需要通过 -P,Docker 主机会自动分配 一个端口转发到指定的端口。 ENV 格式为 ENV 。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。 例如 ENV PG_MAJOR 9.3 ENV PG_VERSION 9.3.4 RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && ... ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH #####ADD 格式为 ADD 。 该命令将复制指定的 到容器中的 。 其中 可以是Dockerfile所在目录的一个相对路 径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。 #####COPY 格式为 COPY 。 复制本地主机的 (为 Dockerfile 所在目录的相对路径)到容器中的 。 当使用本地目录为源目录时,推荐使用 COPY 。 #####ENTRYPOINT 两种格式: • ENTRYPOINT ["executable", "param1", "param2"] • ENTRYPOINT command param1 param2 (shell中执行)。 配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖。 每个 Dockerfile 中只能有一个 ENTRYPOINT ,当指定多个时,只有最后一个起效。 VOLUME 格式为 VOLUME ["/data"] 。 创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。 #####USER 格式为 USER daemon 。 指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。 当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如: roupadd -r postgres && useradd -r -g postgres postgres 。要临时获取管理员权限可以使用 gosu ,而不推 荐 sudo 。 #####WORKDIR 格式为 WORKDIR /path/to/workdir 。 为后续的 RUN 、 CMD 、 ENTRYPOINT 指令配置工作目录。 可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如 RUN g WORKDIR /a WORKDIR b WORKDIR c RUN pwd 则最终路径为 /a/b/c 。 ONBUILD 格式为 ONBUILD [INSTRUCTION] 。 配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。 例如,Dockerfile 使用如下的内容创建了镜像 image-A 。 [...] ONBUILD ADD . /app/src ONBUILD RUN /usr/local/bin/python-build --dir /app/src [...] 如果基于 image-A 创建新的镜像时,新的Dockerfile中使用 FROM image-A 指定基础镜像时,会自动执行 ONBUILD 指令内容,等价于在后面添加了两条指令。 FROM image-A ADD . /app/src RUN /usr/local/bin/python-build --dir /app/src 使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild #####创建镜像 编写完成 Dockerfile 之后,可以通过 docker build 命令来创建镜像。 基本的格式为 docker build [选项] 路径 ,该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下 所有内容发送给 Docker 服务端,由服务端来创建镜像。因此一般建议放置 Dockerfile 的目录为空目录。也可以 通过 .dockerignore 文件(每一行添加一条匹配模式)来让 Docker 忽略路径下的目录和文件。 要指定镜像的标签信息,可以通过 -t 选项,例如 $ sudo docker build -t myrepo/myapp /tmp/test1/ ####docker的底层实现 #####基本架构 Docker 采用了 C/S架构,包括客户端和服务端。 Docker daemon 作为服务端接受来自客户的请求,并处理这 些请求(创建、运行、分发容器)。 客户端和服务端既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。 名称空间是 Linux 内核一个强大的特性。每个容器都有自己单独的名称空间,运行在其中的应用都像是在独立的 操作系统中运行一样。名称空间保证了容器之间彼此互不影响。 #####pid 名称空间 不同用户的进程就是通过 pid 名字空间隔离开的,且不同名字空间中可以有相同 pid。所有的 LXC 进程在 Dock er 中的父进程为Docker进程,每个 LXC 进程具有不同的名字空间。同时由于允许嵌套,因此可以很方便的实现 嵌套的 Docker 容器。 #####net 名称空间 有了 pid 名字空间, 每个名字空间中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net 名字空间实现的, 每个 net 名字空间有独立的 网络设备, IP 地址, 路由表, /proc/net 目录。这样每个容器的 网络就能隔离开来。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docke r0 连接在一起。 #####ipc 名称空间 容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC), 包括信号 量、消息队列和共享内存等。然而同 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 pid 名字空 间中的进程间交互,因此需要在 IPC 资源申请时加入名字空间信息,每个 IPC 资源有一个唯一的 32 位 id。 #####mnt 名称空间 类似 chroot,将一个进程放到一个特定的目录执行。mnt 名字空间允许不同名字空间的进程看到的文件结构不 同,这样每个名字空间 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个名字空间中的容器在 /pr oc/mounts 的信息只包含所在名字空间的 mount point。 #####uts 名称空间 UTS("UNIX Time-sharing System") 名字空间允许每个容器拥有独立的 hostname 和 domain name, 使其在 网络上可以被视作一个独立的节点而非 主机上的一个进程。 #####user 名称空间 每个容器可以有不同的用户和组 id, 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户 #####控制组 控制组(cgroups (http://en.wikipedia.org/wiki/Cgroups) )是 Linux 内核的一个特性,主要用来对共享资源 进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞 争。 控制组技术最早是由 Google 的程序员 2006 年起提出,Linux 内核自 2.6.24 开始支持。 控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。 #####联合文件系统 联合文件系统(UnionFS (http://en.wikipedia.org/wiki/UnionFS) )是一种分层、轻量级并且高性能的文件系 统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统 下(unite several directories into a single virtual filesystem)。 联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作 各种具体的应用镜像。 另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的 效率。 Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个 类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。 Docker 目前支持的联合文件系统种类包括 AUFS, btrfs, vfs 和 DeviceMapper。 #####容器格式 最初,Docker 采用了 LXC 中的容器格式。自 1.20 版本开始,Docker 也开始支持新的 libcontainer #####docker网络的实现 Docker 的网络实现其实就是利用了 Linux 上的网络名字空间和虚拟网络设备(特别是 veth pair) 首先,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子 网之间要进行通信,需要路由机制。 Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。 Linux 通过在内核中进行数 据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存 中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度 要快很多。 Docker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样 的一对接口叫做 veth pair )。 网络配置细节 用户使用 - -net=none 后,可以自行配置网络,让容器达到跟平常一样具有访问网络的权限。通过这个过 程,可以了解 Docker 配置网络的细节。 首先,启动一个 /bin/bash 容器,指定 \--net=none 参数。 $ sudo docker run -i -t \--rm \--net=none base /bin/bash root@63f36fc01b5f:/# 在本地主机查找容器的进程 id,并为它创建网络命名空间。 $ sudo docker inspect -f '{{.State.Pid}}' 63f36fc01b5f 2778 $ pid=2778 $ sudo mkdir -p /var/run/netns $ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid 检查桥接网卡的 IP 和子网掩码信息。 $ ip addr show docker0 21: docker0: ... inet 172.17.42.1/16 scope global docker0 ... 创建一对 “veth pair” 接口 A 和 B,绑定 A 到网桥 docker0 ,并启用它 $ sudo ip link add A type veth peer name B $ sudo brctl addif docker0 A $ sudo ip link set A up 将B放到容器的网络命名空间,命名为 eth0,启动它并配置一个可用 IP(桥接网段)和默认网关。 $ sudo ip link set B netns $pid $ sudo ip netns exec $pid ip link set dev B name eth0 $ sudo ip netns exec $pid ip link set eth0 up $ sudo ip netns exec $pid ip addr add 172.17.42.99/16 dev eth0 $ sudo ip netns exec $pid ip route add default via 172.17.42.1 docker 0 ip netns exec 以上,就是 Docker 配置网络的具体过程。 当容器结束后,Docker 会清空容器,容器内的 eth0 会随网络命名空间一起被清除,A 接口也被自动从 卸载。 此外,用户可以使用 命令来在指定网络名字空间中进行配置,从而配置容器内的网络 nsenter 命令 nsenter 可以访问另一个进程的名字空间 为了连接到容器,你还需要找到容器的第一个进程的 PID,可以通过下面的命令获取。 PID=$(docker inspect --format "{{ .State.Pid }}" ) 通过这个 PID,就可以连接到这个容器: $ nsenter --target $PID --mount --uts --ipc --net --pid 「一键投喂 软糖/蛋糕/布丁/牛奶/冰阔乐!」 赞赏 × 几人行 (๑>ڡ<)☆谢谢老板~ 2元 5元 10元 50元 100元 任意金额 2元 使用微信扫描二维码完成支付 版权声明:本文为作者原创,如需转载须联系作者本人同意,未经作者本人同意不得擅自转载。 Docker笔记 2019-04-02 评论 2125 次浏览