1. 课程介绍

之前一直有同学跟我说我Docker掌握得还可以,但是不知道怎么使用Kubernetes,网上的其他关于 Kubernetes 的课程费用又太高,这节课就是为你们准备的,当然如果你不了解 Docker,不了解 Kubernetes,都没有关系,我们这个课程会从 Docker 入门一步步深入,到 Kubernetes 的进阶使用的。所以大家完全没必要担心。docker基础 - 图1
学完本课程以后,你将会对 Docker 和 Kubernetes 有一个更加深入的认识,我们会讲到:

  • Docker 的一些常用方法,当然我们的重点会在 Kubernetes 上面
  • 会用 kubeadm 来搭建一套 Kubernetes 的集群
  • 理解 Kubernetes 集群的运行原理
  • 常用的一些控制器使用方法
  • 还有 Kubernetes 的一些调度策略
  • Kubernetes的运维
  • 包管理工具 Helm 的使用
  • 最后我们会实现基于 Kubernetes 的 CI/CD

    2. Docker 简介

    什么是 Docker?

    Docker的英文翻译是“搬运工”的意思,他搬运的东西就是我们常说的集装箱Container,Container 里面装的是任意类型的 App,我们的开发人员可以通过 Docker 将App 变成一种标准化的、可移植的、自管理的组件,我们可以在任何主流的操作系统中开发、调试和运行。
    从概念上来看 Docker 和我们传统的虚拟机比较类似,只是更加轻量级,更加方便使,Docker 和虚拟机最主要的区别有以下几点:

  • 虚拟化技术依赖的是物理CPU和内存,是硬件级别的;而我们的 Docker 是构建在操作系统层面的,利用操作系统的容器化技术,所以 Docker 同样的可以运行在虚拟机上面。

  • 我们知道虚拟机中的系统就是我们常说的操作系统镜像,比较复杂;而 Docker 比较轻量级,我们可以用 Docker 部署一个独立的 Redis,就类似于在虚拟机当中安装一个 Redis 应用,但是我们用 Docker 部署的应用是完全隔离的。
  • 我们都知道传统的虚拟化技术是通过快照来保存状态的;而 Docker 引入了类似于源码管理的机制,将容器的快照历史版本一一记录下来,切换成本非常之低。
  • 传统虚拟化技术在构建系统的时候非常复杂;而 Docker 可以通过一个简单的 Dockerfile 文件来构建整个容器,更重要的是 Dockerfile 可以手动编写,这样应用程序开发人员可以通过发布 Dockerfile 来定义应用的环境和依赖,这样对于持续交付非常有利。 docker基础 - 图2

    为啥要用容器?

    应用容器是个啥样子呢,一个做好的应用容器长得就像一个装好了一组特定应用的虚拟机一样,比如我现在想用 Redis,那我就找个装好了 Redis 的容器就可以了,然后运行起来,我就能直接使用了。
    那为什么不能直接安装一个 Redis 呢?肯定是可行的,但是有的时候根据每个人电脑的不同,在安装的时候可能会报出各种各样的错误,万一你的机器中毒了,你的电脑挂了,你所有的服务都需要重新安装。但是有了 Docker 或者说有了容器就不一样了,你就相当于有了一个可以运行起来的虚拟机,只要你能运行容器,Redis 的配置就省了。而且如果你想换个电脑,没问题,很简单,直接把容器”端过来”就可以使用容器里面的服务了。

    Docker Engine

    Docker Engine是一个C/S架构的应用程序,主要包含下面几个组件:

  • 常驻后台进程Dockerd

  • 一个用来和 Dockerd 交互的 REST API Server
  • 命令行CLI接口,通过和 REST API 进行交互(我们经常使用的 docker 命令)

docker基础 - 图3

Docker 架构

Docker 使用 C/S (客户端/服务器)体系的架构,Docker 客户端与 Docker 守护进程通信,Docker 守护进程负责构建,运行和分发 Docker 容器。Docker 客户端和守护进程可以在同一个系统上运行,也可以将 Docker 客户端连接到远程 Docker 守护进程。Docker 客户端和守护进程使用 REST API 通过UNIX套接字或网络接口进行通信。 docker基础 - 图4

  • Docker Damon:dockerd,用来监听 Docker API 的请求和管理 Docker 对象,比如镜像、容器、网络和 Volume。
  • Docker Client:docker,docker client 是我们和 Docker 进行交互的最主要的方式方法,比如我们可以通过 docker run 命令来运行一个容器,然后我们的这个 client 会把命令发送给上面的 Dockerd,让他来做真正事情。
  • Docker Registry:用来存储 Docker 镜像的仓库,Docker Hub 是 Docker 官方提供的一个公共仓库,而且 Docker 默认也是从 Docker Hub 上查找镜像的,当然你也可以很方便的运行一个私有仓库,当我们使用 docker pull 或者 docker run 命令时,就会从我们配置的 Docker 镜像仓库中去拉取镜像,使用 docker push 命令时,会将我们构建的镜像推送到对应的镜像仓库中。
  • Images:镜像,镜像是一个只读模板,带有创建 Docker 容器的说明,一般来说的,镜像会基于另外的一些基础镜像并加上一些额外的自定义功能。比如,你可以构建一个基于 Centos 的镜像,然后在这个基础镜像上面安装一个 Nginx 服务器,这样就可以构成一个属于我们自己的镜像了。
  • Containers:容器,容器是一个镜像的可运行的实例,可以使用 Docker REST API 或者 CLI 来操作容器,容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
  • 底层技术支持:Namespaces(做隔离)、CGroups(做资源限制)、UnionFS(镜像和容器的分层) the-underlying-technology Docker 底层架构分析

    安装

    直接前往官方文档选择合适的平台安装即可,比如我们这里想要在centos系统上安装 Docker,这前往地址https://docs.docker.com/install/linux/docker-ce/centos/根据提示安装即可。
    安装依赖软件包:
    1. $ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
    添加软件仓库,我们这里使用稳定版 Docker,执行下面命令添加 yum 仓库地址:
    1. $ sudo yum-config-manager \
    2. --add-repo \
    3. https://download.docker.com/linux/centos/docker-ce.repo
    然后直接安装即可:
    1. $ sudo yum install docker-ce
    如果要安装指定的版本,可以使用 yum list 列出可用的版本:
    1. $ yum list docker-ce --showduplicates | sort -r
    2. docker-ce.x86_64 18.03.0.ce-1.el7.centos docker-ce-stable
    比如这里可以安装18.03.0.ce版本:
    1. $ sudo yum install docker-ce-18.03.0.ce
    要启动 Docker 也非常简单:
    1. $ sudo systemctl enable docker
    2. $ sudo systemctl start docker
    另外一种安装方式是可以直接下载指定的软件包直接安装即可,前往地址:https://download.docker.com/linux/centos/7/x86_64/stable/Packages/ 找到合适的.rpm包下载,然后安装即可:
    1. $ sudo yum install /path/to/package.rpm

3. 镜像和容器的基本操作

这节课给大家讲解Docker镜像和容器的一些基本操作方法。

获取镜像

之前我们提到过 Docker 官方提供了一个公共的镜像仓库:Docker Hub,我们就可以从这上面获取镜像,获取镜像的命令:docker pull,格式为:

  1. $ docker pull [选项] [Docker Registry 地址[:端口]/]仓库名[:标签]
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号],默认地址是 Docker Hub。
  • 仓库名:这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。比如:

    1. $ docker pull ubuntu:16.04
    2. 16.04: Pulling from library/ubuntu
    3. bf5d46315322: Pull complete
    4. 9f13e0ac480c: Pull complete
    5. e8988b5b3097: Pull complete
    6. 40af181810e7: Pull complete
    7. e6f7c7e5c03e: Pull complete
    8. Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
    9. Status: Downloaded newer image for ubuntu:16.04
  • 上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:16.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 16.04 的镜像。 从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的sha256的摘要,以确保下载一致性。

    运行

    有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:16.04 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。

    1. $ docker run -it --rm \
    2. ubuntu:16.04 \
    3. /bin/bash
    4. root@e7009c6ce357:/# cat /etc/os-release
    5. NAME="Ubuntu"
    6. VERSION="16.04.4 LTS, Trusty Tahr"
    7. ID=ubuntu
    8. ID_LIKE=debian
    9. PRETTY_NAME="Ubuntu 16.04.4 LTS"
    10. VERSION_ID="16.04"
    11. HOME_URL="http://www.ubuntu.com/"
    12. SUPPORT_URL="http://help.ubuntu.com/"
    13. BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

    docker run就是运行容器的命令,具体格式我们会在后面的课程中进行详细讲解,我们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。

  • —rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用--rm可以避免浪费空间。
  • ubuntu:16.04:这是指用 ubuntu:16.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash。

进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了cat /etc/os-release,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 16.04.4 LTS 系统。最后我们通过 exit 退出了这个容器。

列出镜像

  1. $ docker image ls

列表包含了仓库名、标签、镜像 ID、创建时间以及所占用的空间。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。

镜像大小

如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:16.04 镜像大小,在这里是 127 MB,但是在Docker Hub显示的却是 43 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而docker image ls显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是,docker image ls列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。

  1. $ docker system df

新建并启动

所需要的命令主要为docker run。 例如,下面的命令输出一个 “Hello World”,之后终止容器。

  1. $ docker run ubuntu:16.04 /bin/echo 'Hello world'
  2. Hello world

这跟在本地直接执行/bin/echo 'hello world'几乎感觉不出任何区别。下面的命令则启动一个 bash 终端,允许用户进行交互。

  1. $ docker run -t -i ubuntu:16.04 /bin/bash
  2. root@af8bae53bdd3:/#

其中,-t选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i则让容器的标准输入保持打开。 在交互模式下,用户可以通过所创建的终端来输入命令,例如:

  1. root@af8bae53bdd3:/# pwd
  2. /
  3. root@af8bae53bdd3:/# ls
  4. bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

当利用docker run来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

    启动已终止容器

    可以利用docker container start命令,直接将一个已经终止的容器启动运行。
    容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息。
    1. root@ba267838cc1b:/# ps
    2. PID TTY TIME CMD
    3. 1 ? 00:00:00 bash
    4. 11 ? 00:00:00 ps
    可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

    后台运行

    更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加-d参数来实现。下面举两个例子来说明一下。
    如果不使用-d参数运行容器。
    1. $ docker run ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
    2. hello world
    3. hello world
    4. hello world
    5. hello world
    容器会把输出的结果 (STDOUT) 打印到宿主机上面。如果使用了-d参数运行容器。
    1. $ docker run -d ubuntu:16.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
    2. 77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a
    此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

    注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用-d参数启动后会返回一个唯一的 id,也可以通过docker container ls命令来查看容器信息。

  1. $ docker container ls
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 77b2dc01fe0f ubuntu:16.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright
  4. 要获取容器的输出信息,可以通过 docker container logs 命令。
  5. $ docker container logs [container ID or NAMES]
  6. hello world
  7. hello world
  8. hello world
  9. . . .

终止容器

可以使用docker container stop来终止一个运行中的容器。此外,当 Docker 容器中指定的应用终结时,容器也自动终止。
例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。终止状态的容器可以用docker container ls -a 命令看到。例如

  1. $ docker container ls -a
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. ba267838cc1b ubuntu:16.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton

处于终止状态的容器,可以通过docker container start命令来重新启动。
此外,docker container restart命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用-d参数时,容器启动后会进入后台。某些时候需要进入容器进行操作:exec 命令 -i -t 参数
只用-i参数时,由于没有分配伪终端,界面没有我们熟悉的Linux命令提示符,但命令执行结果仍然可以返回。 当-i -t参数一起使用时,则可以看到我们熟悉的 Linux命令提示符。

  1. $ docker run -dit ubuntu:16.04
  2. 69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6
  3. $ docker container ls
  4. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  5. 69d137adef7a ubuntu:16.04 "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles
  6. $ docker exec -i 69d1 bash
  7. ls
  8. bin
  9. boot
  10. dev
  11. ...
  12. $ docker exec -it 69d1 bash
  13. root@69d137adef7a:/#

如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用docker exec的原因。

更多参数说明请使用docker exec --help查看。

删除容器

可以使用docker container rm来删除一个处于终止状态的容器。例如:

  1. $ docker container rm trusting_newton
  2. trusting_newton

也可用使用docker rm容器名来删除,如果要删除一个运行中的容器,可以添加-f参数。Docker 会发送 SIGKILL信号给容器。
docker container ls -a (或者docker ps -a)命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

  1. $ docker container prune

或者

  1. $ docker ps -aq

删除本地镜像

如果要删除本地的镜像,可以使用`docker image rm·命令,其格式为:

  1. $ docker image rm [选项] <镜像1> [<镜像2> ...]

或者

  1. $ docker rmi 镜像名

或者用 ID、镜像名、摘要删除镜像 其中,<镜像> 可以是 镜像短 ID、镜像长 ID、镜像名 或者 镜像摘要。 比如我们有这么一些镜像:

  1. $ docker image ls
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
  4. redis alpine 501ad78535f0 3 weeks ago 21.03 MB
  5. docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
  6. nginx latest e43d811ce2f4 5 weeks ago 181.5 MB

我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker image ls默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。
比如这里,如果我们要删除redis:alpine镜像,可以执行:

  1. $ docker image rm 501
  2. Untagged: redis:alpine
  3. Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
  4. Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
  5. Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
  6. Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
  7. Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
  8. Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
  9. Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7

我们也可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。

  1. $ docker image rm centos
  2. Untagged: centos:latest
  3. Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
  4. Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
  5. Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

docker commit定制镜像

镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。
回顾一下之前我们学到的知识,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。

  1. $ docker run --name webserver -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。
如果是在 Linux 本机运行的 Docker,或者如果使用的是 Docker for Mac、Docker for Windows,那么可以直接访问:http://localhost;如果使用的是 Docker Toolbox,或者是在虚拟机、云服务器上安装的 Docker,则需要将 localhost 换为虚拟机地址或者实际云服务器地址。
直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。
现在,假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 docker exec命令进入容器,修改其内容。

  1. $ docker exec -it webserver bash
  2. root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  3. root@3729b97e8226:/# exit

我们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell。 然后,我们用<h1>Hello, Docker!</h1>覆盖了 /usr/share/nginx/html/index.html的内容。 现在我们再刷新浏览器的话,会发现内容被改变了。
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过docker diff命令看到具体的改动。

  1. $ docker diff webserver
  2. C /root
  3. A /root/.bash_history
  4. C /run
  5. C /usr
  6. C /usr/share
  7. C /usr/share/nginx
  8. C /usr/share/nginx/html
  9. C /usr/share/nginx/html/index.html
  10. C /var
  11. C /var/cache
  12. C /var/cache/nginx
  13. A /var/cache/nginx/client_temp
  14. A /var/cache/nginx/fastcgi_temp
  15. A /var/cache/nginx/proxy_temp
  16. A /var/cache/nginx/scgi_temp
  17. A /var/cache/nginx/uwsgi_temp

现在我们定制好了变化,我们希望能将其保存下来形成镜像。
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
我们可以用下面的命令将容器保存为镜像:

  1. $ docker commit \
  2. --author "海马学院" \
  3. --message "修改了默认首页" \
  4. webserver \
  5. nginx:v2
  6. sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中--author是指定修改的作者,而--message则是记录本次修改的内容。这点和 git 版本控制相似,不过这里这些信息可以省略留空。
我们可以在docker image ls中看到这个新定制的镜像:

  1. $ docker image ls nginx
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. nginx v2 07e334659748 9 seconds ago 181.5 MB
  4. nginx 1.11 05a60462f8ba 12 days ago 181.5 MB
  5. nginx latest e43d811ce2f4 4 weeks ago 181.5 MB

我们还可以用docker history具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,我们会发现新增了我们刚刚提交的这一层。

  1. $ docker history nginx:v2
  2. IMAGE CREATED CREATED BY SIZE COMMENT
  3. 07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页
  4. e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B
  5. <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B
  6. <missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B
  7. <missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB
  8. <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B
  9. <missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B
  10. <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
  11. <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB

新的镜像定制好后,我们可以来运行这个镜像。

  1. $ docker run --name webserv2 -d -p 81:80 nginx:v2

这里我们命名为新的服务为 webserv2,并且映射到 81 端口。如果是 Docker for Mac/Windows 或 Linux 桌面的话,我们就可以直接访问 http://localhost:81 看到结果,其内容应该和之前修改后的 webserver 一样。
至此,我们第一次完成了定制镜像,使用的是docker commit命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。

注意: docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 docker commit 定制镜像,定制镜像应该使用Dockerfile来完成。如果你想要定制镜像请查看下一小节。

4. Dockerfile 定制镜像

从前面一节的docker commit的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件等信息,但是命令毕竟只是命令,每次定制都得去重复执行这个命令,而且还不够直观,如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么这些问题不就都可以解决了吗?对的,这个脚本就是我们说的Dockerfile

介绍

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
还以之前定制 nginx 镜像为例,这次我们使用 Dockerfile 来定制。在一个空白目录中,建立一个文本文件,并命名为 Dockerfile:

  1. $ mkdir mynginx
  2. $ cd mynginx
  3. $ touch Dockerfile

其内容为:

  1. FROM nginx
  2. RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM 和 RUN。

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而FROM就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
Docker Store上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

  1. FROM scratch
  2. ...

如果你以scratch为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。有的同学可能感觉很奇怪,没有任何基础镜像,我怎么去执行我的程序呢,其实对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN指令是用来执行命令行命令的。由于命令行的强大能力,RUN指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。

    1. RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式。 既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

    1. FROM debian:jessie
    2. RUN apt-get update
    3. RUN apt-get install -y gcc libc6-dev make
    4. RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
    5. RUN mkdir -p /usr/src/redis
    6. RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
    7. RUN make -C /usr/src/redis
    8. RUN make -C /usr/src/redis install

    之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
    而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

    Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

上面的 Dockerfile 正确的写法应该是这样:

  1. FROM debian:jessie
  2. RUN buildDeps='gcc libc6-dev make' \
  3. && apt-get update \
  4. && apt-get install -y $buildDeps \
  5. && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
  6. && mkdir -p /usr/src/redis \
  7. && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
  8. && make -C /usr/src/redis \
  9. && make -C /usr/src/redis install \
  10. && rm -rf /var/lib/apt/lists/* \
  11. && rm redis.tar.gz \
  12. && rm -r /usr/src/redis \
  13. && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用&&将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加\的命令换行方式,以及行首#进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。 很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile的内容,那么让我们来构建这个镜像吧。在 Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 9cdc27646c7b,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c,随后删除了所用到的这个容器 9cdc27646c7b。这里我们使用了 docker build命令进行镜像构建。其格式为:

$ docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3,构建成功后,我们可以像之前运行 nginx:v2 那样来运行这个镜像,其结果会和 nginx:v2 一样。

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个..表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?
首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json。
因此,COPY这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令docker build -t nginx:v3 .中的这个.,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解构建上下文对于镜像构建是很重要的,可以避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个.dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。
这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用-f ../Dockerfile.php参数指定某个文件作为 Dockerfile。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

迁移镜像

Docker 还提供了docker loaddocker save命令,用以将镜像保存为一个 tar 文件,然后传输到另一个位置上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该直接使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。
使用docker save命令可以将镜像保存为归档文件。比如我们希望保存这个 alpine 镜像。

$ docker image ls alpine
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine              latest              baa5d63471ea        5 weeks ago         4.803 MB

保存镜像的命令为:

$ docker save alpine | gzip > alpine-latest.tar.gz

然后我们将 alpine-latest.tar.gz 文件复制到了到了另一个机器上,可以用下面这个命令加载镜像:

$ docker load -i alpine-latest.tar.gz
Loaded image: alpine:latest

如果我们结合这两个命令以及 ssh 甚至 pv 的话,利用 Linux 强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:

docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'

5. 私有镜像仓库

这节课给大家讲讲私有镜像仓库的使用。

Docker Hub

目前 Docker 官方维护了一个公共仓库Docker Hub,大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现。如果你觉得拉取 Docker Hub 的镜像比较慢的话,我们可以配置一个镜像加速器:http://docker-cn.com/,当然国内大部分云厂商都提供了相应的加速器,简单配置即可。

注册

你可以在 https://cloud.docker.com 免费注册一个 Docker 账号。

登录

通过执行docker login命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub。

注销

你可以通过docker logout退出登录。 拉取镜像

拉取镜像

你可以通过docker search命令来查找官方仓库中的镜像,并利用docker pull命令来将它下载到本地。
例如以 centos 为关键词进行搜索:

$ docker search centos
NAME                                            DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
centos                                          The official build of CentOS.                   465       [OK]
tianon/centos                                   CentOS 5 and 6, created using rinse instea...   28
blalor/centos                                   Bare-bones base CentOS 6.5 image                6                    [OK]
saltstack/centos-6-minimal                                                                      6                    [OK]
tutum/centos-6.4                                DEPRECATED. Use tutum/centos:6.4 instead. ...   5                    [OK]

可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。
官方的镜像说明是官方项目组创建和维护的,automated资源允许用户验证镜像的来源和内容。
根据是否是官方提供,可将镜像资源分为两类。

  • 一种是类似 centos 这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。
  • 还有一种类型,比如 tianon/centos 镜像,它是由 Docker 的用户创建并维护的,往往带有用户名称前缀。可以通过前缀username/来指定使用某个用户提供的镜像,比如 tianon 用户。

另外,在查找的时候通过--filter=stars=N参数可以指定仅显示收藏数量为 N 以上的镜像。下载官方 centos 镜像到本地。

$ docker pull centos
Pulling repository centos
0b443ba03958: Download complete
539c0211cd76: Download complete
511136ea3c5a: Download complete
7064731afe90: Download complete

推送镜像

用户也可以在登录后通过docker push命令来将自己的镜像推送到 Docker Hub。以下命令中的 username 请替换为你的 Docker 账号用户名。

$ docker tag ubuntu:17.10 username/ubuntu:17.10
$ docker image ls
REPOSITORY                                               TAG                    IMAGE ID            CREATED             SIZE
ubuntu                                                   17.10                  275d79972a86        6 days ago          94.6MB
username/ubuntu                                          17.10                  275d79972a86        6 days ago          94.6MB
$ docker push username/ubuntu:17.10
$ docker search username
NAME                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
username/ubuntu

私有仓库

有时候使用 Docker Hub 这样的公共仓库可能不方便,用户可以创建一个本地仓库供私人使用。
docker-registry是官方提供的工具,可以用于构建私有的镜像仓库。本文内容基于 docker-registry v2.x 版本。你可以通过获取官方 registry 镜像来运行。

$ docker run -d -p 5000:5000 --restart=always --name registry registry

这将使用官方的registry镜像来启动私有仓库。默认情况下,仓库会被创建在容器的/var/lib/registry目录下。你可以通过 -v 参数来将镜像文件存放在本地的指定路径。例如下面的例子将上传的镜像放到本地的 /opt/data/registry 目录。

$ docker run -d \
    -p 5000:5000 \
    -v /opt/data/registry:/var/lib/registry \
    registry

在私有仓库上传、搜索、下载镜像

创建好私有仓库之后,就可以使用docker tag来标记一个镜像,然后推送它到仓库。例如私有仓库地址为 127.0.0.1:5000。先在本机查看已有的镜像。

$ docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB

使用docker tag将 ubuntu:latest 这个镜像标记为 127.0.0.1:5000/ubuntu:latest。 格式为 docker tag IMAGE[:TAG] [REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]

$ docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest
$ docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
127.0.0.1:5000/ubuntu:latest      latest              ba5877dc9bec        6 weeks ago         192.7 MB

使用docker push上传标记的镜像。

$ docker push 127.0.0.1:5000/ubuntu:latest
The push refers to repository [127.0.0.1:5000/ubuntu]
373a30c24545: Pushed
a9148f5200b0: Pushed
cdd3de0940ab: Pushedfc56279bbb33: Pushed
b38367233d37: Pushed
2aebd096e0e2: Pushed
latest: digest: sha256:fe4277621f10b5026266932ddf760f5a756d2facd505a94d2da12f4f52f71f5a size: 1568

curl查看仓库中的镜像。

$ curl 127.0.0.1:5000/v2/_catalog
{"repositories":["ubuntu"]}

这里可以看到 {“repositories”:[“ubuntu”]},表明镜像已经被成功上传了。
先删除已有镜像,再尝试从私有仓库中下载这个镜像。

$ docker image rm 127.0.0.1:5000/ubuntu:latest
$ docker pull 127.0.0.1:5000/ubuntu:latest
Pulling repository 127.0.0.1:5000/ubuntu:latest
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete
$ docker image ls
REPOSITORY                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
127.0.0.1:5000/ubuntu:latest       latest              ba5877dc9bec        6 weeks ago         192.7 MB

注意事项

如果你不想使用 127.0.0.1:5000 作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。你就得把例如 192.168.199.100:5000 这样的内网地址作为私有仓库地址,这时你会发现无法成功推送镜像。
这是因为 Docker 默认不允许非 HTTPS 方式推送镜像。我们可以通过 Docker 的配置选项来取消这个限制。

Ubuntu 14.04, Debian 7 Wheezy

对于使用 upstart 的系统而言,编辑/etc/default/docker文件,在其中的DOCKER_OPTS中增加如下内容:

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com --insecure-registries=192.168.199.100:5000"

重新启动服务:

$ sudo service docker restart

Ubuntu 16.04+, Debian 8+, centos 7

对于使用 systemd 的系统,请在/etc/docker/daemon.json中写入如下内容(如果文件不存在请新建该文件)

{
  "registry-mirror": [
    "https://registry.docker-cn.com"
  ],
  "insecure-registries": [
    "192.168.199.100:5000"
  ]
}

注意:该文件必须符合json规范,否则 Docker 将不能启动。

其他

对于 Docker for Windows、Docker for Mac 在设置中编辑daemon.json增加和上边一样的字符串即可。

6. 数据共享与持久化

这一节介绍如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:

  • 数据卷(Data Volumes)
  • 挂载主机目录 (Bind mounts)

    数据卷

    数据卷是一个可供一个或多个容器使用的特殊目录,它绕过UFS,可以提供很多有用的特性:

  • 数据卷 可以在容器之间共享和重用

  • 对 数据卷 的修改会立马生效
  • 对 数据卷 的更新,不会影响镜像
  • 数据卷 默认会一直存在,即使容器被删除

    注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。

选择 -v 还是 -–mount 参数: Docker 新用户应该选择--mount参数,经验丰富的 Docker 使用者对-v或者 --volume已经很熟悉了,但是推荐使用--mount参数。
创建一个数据卷:

$ docker volume create my-vol

查看所有的 数据卷:

$ docker volume ls
local               my-vol

在主机里使用以下命令可以查看指定 数据卷 的信息

$ docker volume inspect my-vol
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

启动一个挂载数据卷的容器:在用docker run命令的时候,使用--mount标记来将 数据卷 挂载到容器里。在一次docker run中可以挂载多个 数据卷。下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /webapp 目录。

$ docker run -d -P \
    --name web \
    # -v my-vol:/wepapp \
    --mount source=my-vol,target=/webapp \
    training/webapp \
    python app.py

查看数据卷的具体信息:在主机里使用以下命令可以查看 web 容器的信息

$ docker inspect web
...
"Mounts": [
    {
        "Type": "volume",
        "Name": "my-vol",
        "Source": "/var/lib/docker/volumes/my-vol/_data",
        "Destination": "/app",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],
...

删除数据卷:

$ docker volume rm my-vol

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用docker rm -v这个命令。 无主的数据卷可能会占据很多空间,要清理请使用以下命令

$ docker volume prune

挂载主机目录

选择 -v 还是 -–mount 参数: Docker 新用户应该选择 —mount 参数,经验丰富的 Docker 使用者对 -v 或者 —volume 已经很熟悉了,但是推荐使用 —mount 参数。
挂载一个主机目录作为数据卷:使用 --mount 标记可以指定挂载一个本地主机的目录到容器中去。

$ docker run -d -P \
    --name web \
    # -v /src/webapp:/opt/webapp \
    --mount type=bind,source=/src/webapp,target=/opt/webapp \
    training/webapp \
    python app.py

上面的命令加载主机的 /src/webapp 目录到容器的 /opt/webapp目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 —mount 参数时如果本地目录不存在,Docker 会报错。
Docker 挂载主机目录的默认权限是 读写,用户也可以通过增加readonly指定为 只读。

$ docker run -d -P \
    --name web \
    # -v /src/webapp:/opt/webapp:ro \
    --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \
    training/webapp \
    python app.py

加了readonly之后,就挂载为 只读 了。如果你在容器内 /opt/webapp 目录新建文件,会显示如下错误:

/opt/webapp # touch new.txt
touch: new.txt: Read-only file system

查看数据卷的具体信息:在主机里使用以下命令可以查看 web 容器的信息

$ docker inspect web
...
"Mounts": [
    {
        "Type": "bind",
        "Source": "/src/webapp",
        "Destination": "/opt/webapp",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
],

挂载一个本地主机文件作为数据卷:--mount标记也可以从主机挂载单个文件到容器中

$ docker run --rm -it \
   # -v $HOME/.bash_history:/root/.bash_history \
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
   ubuntu:17.10 \
   bash
root@2affd44b4667:/# history
1  ls
2  diskutil list

这样就可以记录在容器输入过的命令了。

7. Docker 的网络模式

Bridge模式

Docker进程启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。从docker0子网中分配一个 IP 给容器使用,并设置 docker0 的 IP 地址为容器的默认网关。在主机上创建一对虚拟网卡veth pair设备,Docker 将 veth pair 设备的一端放在新创建的容器中,并命名为eth0(容器的网卡),另一端放在主机中,以vethxxx这样类似的名字命名,并将这个网络设备加入到 docker0 网桥中。可以通过brctl show命令查看。
bridge模式是 docker 的默认网络模式,不写–net参数,就是bridge模式。使用docker run -p时,docker 实际是在iptables做了DNAT规则,实现端口转发功能。可以使用iptables -t nat -vnL查看。bridge模式如下图所示:docker基础 - 图5
演示:

$ docker run -tid --net=bridge --name docker_bri1 \
             ubuntu-base:v3
             docker run -tid --net=bridge --name docker_bri2 \
             ubuntu-base:v3 
$ brctl show
$ docker exec -ti docker_bri1 /bin/bash
$ ifconfig –a
$ route –n

如果你之前有 Docker 使用经验,你可能已经习惯了使用--link参数来使容器互联。
随着 Docker 网络的完善,强烈建议大家将容器加入自定义的 Docker 网络来连接多个容器,而不是使用 —link 参数。
下面先创建一个新的 Docker 网络。

$ docker network create -d bridge my-net

-d参数指定 Docker 网络类型,有 bridge overlay。其中 overlay 网络类型用于 Swarm mode,在本小节中你可以忽略它。
运行一个容器并连接到新建的 my-net 网络

$ docker run -it --rm --name busybox1 --network my-net busybox sh

打开新的终端,再运行一个容器并加入到 my-net 网络

$ docker run -it --rm --name busybox2 --network my-net busybox sh

再打开一个新的终端查看容器信息

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
b47060aca56b        busybox             "sh"                11 minutes ago      Up 11 minutes                           busybox2
8720575823ec        busybox             "sh"                16 minutes ago      Up 16 minutes                           busybox1

下面通过 ping 来证明 busybox1 容器和 busybox2 容器建立了互联关系。 在 busybox1 容器输入以下命令

/ # ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.072 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.118 ms

用 ping 来测试连接 busybox2 容器,它会解析成 172.19.0.3。 同理在 busybox2 容器执行 ping busybox1,也会成功连接到。

/ # ping busybox1
PING busybox1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.064 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.143 ms

这样,busybox1 容器和 busybox2 容器建立了互联关系。
如果你有多个容器之间需要互相连接,推荐使用Docker Compose

Host 模式

如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个 Network Namespace。容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。但是,容器的其他方面,如文件系统、进程列表等还是和宿主机隔离的。 Host模式如下图所示:
docker基础 - 图6
演示:

$ docker run -tid --net=host --name docker_host1 ubuntu-base:v3
$ docker run -tid --net=host --name docker_host2 ubuntu-base:v3
$ docker exec -ti docker_host1 /bin/bash
$ docker exec -ti docker_host1 /bin/bash
$ ifconfig –a
$ route –n

Container 模式

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。 Container模式示意图: docker基础 - 图7演示:

$ docker run -tid --net=container:docker_bri1 \
              --name docker_con1 ubuntu-base:v3
$ docker exec -ti docker_con1 /bin/bash
$ docker exec -ti docker_bri1 /bin/bash
$ ifconfig –a
$ route -n

None模式

使用none模式,Docker 容器拥有自己的 Network Namespace,但是,并不为Docker 容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。 None模式示意图: docker基础 - 图8演示:

$ docker run -tid --net=none --name \
                docker_non1 ubuntu-base:v3
$ docker exec -ti docker_non1 /bin/bash
$ ifconfig –a
$ route -n

Docker 的跨主机通信我们这里就先暂时不讲解,我们在后面的Kubernetes课程当中会用到。

8. Docker Compose

介绍

Docker ComposeDocker官方编排(Orchestration)项目之一,负责快速的部署分布式应用。其代码目前在https://github.com/docker/compose上开源。Compose 定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」,其前身是开源项目Fig
前面我们已经学习过使用一个Dockerfile模板文件,可以很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器或者缓存服务容器,甚至还包括负载均衡容器等。Compose 恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose 中有两个重要的概念:

  • 服务 (service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

Compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。Compose 项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。所以只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

安装与卸载

Compose支持 Linux、macOS、Windows 10 三大平台。Compose 可以通过 Python 的包管理工具pip进行安装,也可以直接下载编译好的二进制文件使用,甚至能够直接在 Docker 容器中运行。前两种方式是传统方式,适合本地环境下安装使用;最后一种方式则不破坏系统环境,更适合云计算场景。Docker for Mac 、Docker for Windows 自带 docker-compose 二进制文件,安装 Docker 之后可以直接使用。

$ docker-compose --version
docker-compose version 1.17.1, build 6d101fb

二进制安装

在 Linux 上的也安装十分简单,从 官方 GitHub Release 处直接下载编译好的二进制文件即可。例如,在 Linux 64 位系统上直接下载对应的二进制包。

$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

PIP 安装

注: x86_64 架构的 Linux 建议按照上边的方法下载二进制包进行安装,如果您计算机的架构是 ARM (例如,树莓派),再使用 pip 安装。

这种方式是将 Compose 当作一个 Python 应用来从 pip 源中安装。执行安装命令:

$ sudo pip install -U docker-compose
Collecting docker-compose
  Downloading docker-compose-1.17.1.tar.gz (149kB): 149kB downloaded
...
Successfully installed docker-compose cached-property requests texttable websocket-client docker-py dockerpty six enum34 backports.ssl-match-hostname ipaddress

bash 补全命令:

$ curl -L https://raw.githubusercontent.com/docker/compose/1.8.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

容器中执行

Compose 既然是一个 Python 应用,自然也可以直接用容器来执行它。

$ curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

实际上,查看下载的run.sh脚本内容,如下:

set -e
VERSION="1.8.0"
IMAGE="docker/compose:$VERSION"
# Setup options for connecting to docker hostif [ -z "$DOCKER_HOST" ]; then
    DOCKER_HOST="/var/run/docker.sock"fiif [ -S "$DOCKER_HOST" ]; then
    DOCKER_ADDR="-v $DOCKER_HOST:$DOCKER_HOST -e DOCKER_HOST"else
    DOCKER_ADDR="-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH"fi
# Setup volume mounts for compose config and contextif [ "$(pwd)" != '/' ]; then
    VOLUMES="-v $(pwd):$(pwd)"fiif [ -n "$COMPOSE_FILE" ]; then
    compose_dir=$(dirname $COMPOSE_FILE)fi# TODO: also check --file argumentif [ -n "$compose_dir" ]; then
    VOLUMES="$VOLUMES -v $compose_dir:$compose_dir"fiif [ -n "$HOME" ]; then
    VOLUMES="$VOLUMES -v $HOME:$HOME -v $HOME:/root" # mount $HOME in /root to share docker.configfi
# Only allocate tty if we detect oneif [ -t 1 ]; then
    DOCKER_RUN_OPTIONS="-t"fiif [ -t 0 ]; then
    DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"fi
exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@"

可以看到,它其实是下载了docker/compose镜像并运行。

卸载

如果是二进制包方式安装的,删除二进制文件即可。

$ sudo rm /usr/local/bin/docker-compose

如果是通过 pip 安装的,则执行如下命令即可删除。

$ sudo pip uninstall docker-compose

使用

下面我们用 Python 来建立一个能够记录页面访问次数的 web 网站。 新建文件夹,在该目录中编写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)

接着编写Dockerfile文件,内容为:

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

然后是编写docker-compose.yml文件,这个是 Compose 使用的主模板文件。

version: '3'
services:
  web:    
  build: .    
  ports:    
  - "5000:5000"
  volumes:
       - .:/code
  redis:    
  image: "redis:alpine"

运行 compose 项目:

$ docker-compose up

此时访问本地 5000 端口,每次刷新页面,计数就会加 1。

Compose 命令

对于 Compose 来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有的服务都会受到命令影响。
执行docker-compose [COMMAND] —help或者docker-compose help [COMMAND]可以查看具体某个命令的使用格式。
docker-compose 命令的基本的使用格式是:

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令选项:

  • -f, —file FILE 指定使用的 Compose 模板文件,默认为 docker-compose.yml,可以多次指定。
  • -p, —project-name NAME 指定项目名称,默认将使用所在目录名称作为项目名。
  • —x-networking 使用 Docker 的可拔插网络后端特性
  • —x-network-driver DRIVER 指定网络后端的驱动,默认为 bridge
  • —verbose 输出更多调试信息。
  • -v, —version 打印版本并退出。

build 格式为docker-compose build [options] [SERVICE…]。 构建(重新构建)项目中的服务容器。服务容器一旦构建后,将会带上一个标记名,例如对于 web 项目中的一个 db 容器,可能是 web_db。可以随时在项目目录下运行docker-compose build来重新构建服务。选项包括:

  • —force-rm 删除构建过程中的临时容器。
  • —no-cache 构建镜像过程中不使用 cache(这将加长构建过程)。
  • —pull 始终尝试通过 pull 来获取更新版本的镜像。

config: 验证 Compose 文件格式是否正确,若正确则显示配置,若格式错误显示错误原因。
down:此命令将会停止 up 命令所启动的容器,并移除网络
exec:进入指定的容器。
help:获得一个命令的帮助。
images:列出 Compose 文件中包含的镜像。
kill:格式为docker-compose kill [options] [SERVICE…]。通过发送SIGKILL信号来强制停止服务容器。支持通过-s参数来指定发送的信号,例如通过如下指令发送SIGINT信号。

$ docker-compose kill -s SIGINT

logs:格式为docker-compose logs [options] [SERVICE…],查看服务容器的输出。默认情况下,docker-compose 将对不同的服务输出使用不同的颜色来区分。可以通过 --no-color来关闭颜色。该命令在调试问题的时候十分有用。
pause:格式为docker-compose pause [SERVICE…],暂停一个服务容器。
port:格式为docker-compose port [options] SERVICE PRIVATE_PORT,打印某个容器端口所映射的公共端口。选项:

  • —protocol=proto 指定端口协议,tcp(默认值)或者 udp。
  • —index=index 如果同一服务存在多个容器,指定命令对象容器的序号(默认为 1)。

ps:格式为docker-compose ps [options] [SERVICE…],列出项目中目前的所有容器。选项:

  • -q只打印容器的 ID 信息。

pull:格式为docker-compose pull [options] [SERVICE...],拉取服务依赖的镜像。选项:

  • —ignore-pull-failures 忽略拉取镜像过程中的错误。

push:推送服务依赖的镜像到 Docker 镜像仓库。
restart:格式为docker-compose restart [options] [SERVICE…],重启项目中的服务。选项:

  • -t, —timeout TIMEOUT 指定重启前停止容器的超时(默认为 10 秒)。

rm:格式为docker-compose rm [options] [SERVICE…],删除所有(停止状态的)服务容器。推荐先执行 docker-compose stop命令来停止容器。选项:

  • -f, —force 强制直接删除,包括非停止状态的容器。一般尽量不要使用该选项。
  • -v 删除容器所挂载的数据卷。

run:格式为docker-compose run [options] [-p PORT…] [-e KEY=VAL…] SERVICE [COMMAND] [ARGS…],在指定服务上执行一个命令。例如:

$ docker-compose run ubuntu ping docker.com

将会启动一个 ubuntu 服务容器,并执行 ping docker.com 命令。默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中。
该命令类似启动容器后运行指定的命令,相关卷、链接等等都将会按照配置自动创建。
给定命令将会覆盖原有的自动运行命令; 不会自动创建端口,以避免冲突。
如果不希望自动启动关联的容器,可以使用--no-deps选项,例如:

$ docker-compose run --no-deps web python manage.py shell

将不会启动 web 容器所关联的其它容器,选项:

  • -d 后台运行容器。
  • —name NAME 为容器指定一个名字。
  • —entrypoint CMD 覆盖默认的容器启动指令。
  • -e KEY=VAL 设置环境变量值,可多次使用选项来设置多个环境变量。
  • -u, —user=”” 指定运行容器的用户名或者 uid。
  • —no-deps 不自动启动关联的服务容器。
  • —rm 运行命令后自动删除容器,d 模式下将忽略。
  • -p, —publish=[] 映射容器端口到本地主机。
  • —service-ports 配置服务端口并映射到本地主机。
  • -T 不分配伪 tty,意味着依赖 tty 的指令将无法运行。

scale:格式为docker-compose scale [options] [SERVICE=NUM…],设置指定服务运行的容器个数。 通过 service=num 的参数来设置数量。例如:

$ docker-compose scale web=3 db=2

将启动 3 个容器运行 web 服务,2 个容器运行 db 服务。
一般的,当指定数目多于该服务当前实际运行容器,将新创建并启动容器;反之,将停止容器。选项:

  • -t, —timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

start:格式为docker-compose start [SERVICE…],启动已经存在的服务容器。
stop:格式为docker-compose stop [options] [SERVICE…], 停止已经处于运行状态的容器,但不删除它。通过docker-compose start可以再次启动这些容器。选项:

  • -t, —timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

top:查看各个服务容器内运行的进程。
unpause:格式为docker-compose unpause [SERVICE…],恢复处于暂停状态中的服务。
up:格式为docker-compose up [options] [SERVICE…],该命令十分强大,它将尝试自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作。链接的服务都将会被自动启动,除非已经处于运行状态。 可以说,大部分时候都可以直接通过该命令来启动一个项目。
默认情况,docker-compose up启动的容器都在前台,控制台将会同时打印所有容器的输出信息,可以很方便进行调试。 当通过 Ctrl-C 停止命令时,所有容器将会停止。
如果使用docker-compose up -d,将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。
默认情况,如果服务容器已经存在,docker-compose up将会尝试停止容器,然后重新创建(保持使用 volumes-from 挂载的卷),以保证新启动的服务匹配 docker-compose.yml 文件的最新内容。如果用户不希望容器被停止并重新创建,可以使用 docker-compose up --no-recreate。这样将只会启动处于停止状态的容器,而忽略已经运行的服务。
如果用户只想重新部署某个服务,可以使用docker-compose up --no-deps -d <SERVICE_NAME>来重新创建服务并后台停止旧服务,启动新服务,并不会影响到其所依赖的服务。选项:

  • -d 在后台运行服务容器。
  • —no-color 不使用颜色来区分不同的服务的控制台输出。
  • —no-deps 不启动服务所链接的容器。
  • —force-recreate 强制重新创建容器,不能与--no-recreate同时使用。 --no-recreate如果容器已经存在了,则不重新创建,不能与 --force-recreate同时使用。 --no-build不自动构建缺失的服务镜像。
  • -t, —timeout TIMEOUT 停止容器时候的超时(默认为 10 秒)。

9. Docker Machine

Docker MachineDocker官方编排(Orchestration)项目之一,负责在多种平台上快速安装 Docker 环境。
Docker Machine项目基于Go语言实现,目前在Github上进行维护。
Docker Machine是 Docker 官方提供的一个工具,它可以帮助我们在远程的机器上安装 Docker,或者在虚拟机 host 上直接安装虚拟机并在虚拟机中安装 Docker。我们还可以通过 docker-machine命令来管理这些虚拟机和 Docker。
本章将介绍 Docker Machine 的安装及使用。

安装

Docker Machine 可以在多种操作系统平台上安装,包括 Linux、macOS,以及 Windows。

macOS、Windows

Docker for Mac、Docker for Windows 自带 docker-machine 二进制包,安装之后即可使用。查看版本信息。

$ docker-machine -v
docker-machine version 0.13.0, build 9ba6da9

Linux

在 Linux 上的也安装十分简单,从官方 GitHub Release处直接下载编译好的二进制文件即可。 例如,在 Linux 64 位系统上直接下载对应的二进制包。

$ sudo curl -L https://github.com/docker/machine/releases/download/v0.13.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine
$ sudo chmod +x /usr/local/bin/docker-machine

完成后,查看版本信息。

$ docker-machine -v
docker-machine version 0.13.0, build 9ba6da9

使用

Docker Machine 支持多种后端驱动,包括虚拟机、本地主机和云平台等。

创建本地主机实例Virtualbox 驱动

使用virtualbox类型的驱动,创建一台 Docker 主机,命名为 test。

$ docker-machine create -d virtualbox test

你也可以在创建时加上如下参数,来配置主机或者主机上的 Docker。

  • —engine-opt dns=114.114.114.114配置 Docker 的默认 DNS
  • —engine-registry-mirror https://registry.docker-cn.com配置 Docker 的仓库镜像
  • —virtualbox-memory 2048 配置主机内存
  • —virtualbox-cpu-count 2 配置主机 CPU

更多参数请使用docker-machine create --driver virtualbox --help命令查看。

$ docker-machine create -d generic \
    --generic-ip-address=123.59.188.19 \
    --generic-ssh-user=root \
    --generic-ssh-key ~/.ssh/id_rsa \
    dev

MacOS xhyve 驱动

xhyve 驱动 GitHub: https://github.com/zchee/docker-machine-driver-xhyve,xhyveMacOS上轻量化的虚拟引擎,使用其创建的 Docker Machine 较 VirtualBox 驱动创建的运行效率要高。

$ brew install docker-machine-driver-xhyve
......
$ docker-machine create \
      -d xhyve \
      # --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso \
      --engine-opt dns=114.114.114.114 \
      --engine-registry-mirror https://registry.docker-cn.com \
      --xhyve-memory-size 2048 \
      --xhyve-rawdisk \
      --xhyve-cpu-count 2 \
      xhyve

注意:非首次创建时建议加上—xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso参数,避免每次创建时都从 GitHub 下载 ISO 镜像。

更多参数请使用docker-machine create --driver xhyve --help命令查看。

Windows 10

Windows 10安装Docker for Windows之后不能再安装VirtualBox,也就不能使用 virtualbox 驱动来创建 Docker Machine,我们可以选择使用 hyperv 驱动。

$ docker-machine create --driver hyperv vm

更多参数请使用docker-machine create --driver hyperv --help命令查看。

使用介绍

创建好主机之后,查看主机

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER       ERRORStest      -        virtualbox   Running   tcp://192.168.99.187:2376           v17.10.0-ce

创建主机成功后,可以通过env命令来让后续操作对象都是目标主机。

$ docker-machine env test

后续根据提示在命令行输入命令之后就可以操作 test 主机。也可以通过SSH登录到主机。

$ docker-machine ssh test
docker@test:~$ docker --version
Docker version 17.10.0-ce, build f4ffd25

连接到主机之后你就可以在其上使用 Docker 了。

官方支持驱动

通过-d选项可以选择支持的驱动类型:

  • amazonec2
  • azure
  • digitalocean
  • exoscale
  • generic
  • google
  • hyperv
  • none
  • openstack
  • rackspace
  • softlayer
  • virtualbox
  • vmwarevcloudair
  • vmwarefusion
  • vmwarevsphere

    操作命令

  • active 查看活跃的 Docker 主机

  • config 输出连接的配置信息
  • create 创建一个 Docker 主机
  • env 显示连接到某个主机需要的环境变量
  • inspect 输出主机更多信息
  • ip 获取主机地址
  • kill 停止某个主机
  • ls 列出所有管理的主机
  • provision 重新设置一个已存在的主机
  • regenerate-certs 为某个主机重新生成 TLS 认证信息
  • restart 重启主机
  • rm 删除某台主机
  • ssh SSH 到主机上执行命令
  • scp 在主机之间复制文件
  • mount 挂载主机目录到本地
  • start 启动一个主机
  • status 查看主机状态
  • stop 停止一个主机
  • upgrade 更新主机 Docker 版本为最新
  • url 获取主机的 URL
  • version 输出 docker-machine 版本信息
  • help 输出帮助信息

每个命令,又带有不同的参数,可以通过如下命令来查看具体的用法:

$ docker-machine COMMAND --help

10. Docker Swarm

基本概念

Swarm是使用SwarmKit构建的 Docker 引擎内置(原生)的集群管理和编排工具。Docker Swarm是 Docker 官方三剑客项目之一,提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。
使用它,用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。Swarm mode 内置 kv 存储功能,提供了众多的新特性,比如:具有容错能力的去中心化设计、内置服务发现、负载均衡、路由网格、动态伸缩、滚动更新、安全传输等。使得 Docker 原生的 Swarm 集群具备与MesosKubernetes竞争的实力。使用 Swarm 集群之前需要了解以下几个概念。

节点

运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点 (node) 。节点分为管理 (manager) 节点和工作 (worker) 节点
管理节点用于Swarm集群的管理,docker swarm命令基本只能在管理节点执行(节点退出集群命令docker swarm leave可以在工作节点执行)。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为leader,leader 通过raft协议实现。
工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。来自Docker官网的这张图片形象的展示了集群中管理节点与工作节点的关系。 docker基础 - 图9

服务和任务

任务(Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器;服务(Services)是指一组任务的集合,服务定义了任务的属性。服务有两种模式:

  • replicated services按照一定规则在各个工作节点上运行指定个数的任务。
  • global services每个工作节点上运行一个任务

两种模式通过docker service create--mode参数指定。来自 Docker 官网的这张图片形象的展示了容器、任务、服务的关系。 docker基础 - 图10

初始化集群

我们这里利用上一节的docker machine来充当集群的主机,首先先创建一个manager节点,然后在该节点上执行初始化集群命令:

☁  ~  docker-machine create -d virtualbox manager
Running pre-create checks...
Creating machine...
(manager) Copying /Users/ych/.docker/machine/cache/boot2docker.iso to /Users/ych/.docker/machine/machines/manager/boot2docker.iso...
(manager) Creating VirtualBox VM...
(manager) Creating SSH key...
(manager) Starting the VM...
(manager) Check network to re-create if needed...
(manager) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env manager
☁  ~  docker-machine env manager
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.101:2376"
export DOCKER_CERT_PATH="/Users/ych/.docker/machine/machines/manager"
export DOCKER_MACHINE_NAME="manager"
# Run this command to configure your shell:
# eval $(docker-machine env manager)
☁  ~  eval $(docker-machine env manager)

☁  ~  docker-machine ssh manager
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 18.03.1-ce, build HEAD : cb77972 - Thu Apr 26 16:40:36 UTC 2018
Docker version 18.03.1-ce, build 9ee9f40
docker@manager:~$ docker swarm init --advertise-addr 192.168.99.101
Swarm initialized: current node (3gsjpckj5ag1vvdg44fgzylow) is now a manager.
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-1aqikkhsz91l4n7k9ig3xinjz0iv0fh4gcrlhp9mk3643rblca-aqgqldlrw33k8heiao7yx27w5 192.168.99.101:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

执行docker swarm init命令的节点自动成为管理节点。

增加工作节点

管理节点初始化完成后,然后同样的用docker-machine创建工作节点,然后将其加入到管理节点之中去即可:

☁  ~  docker-machine create -d virtualbox worker1
Running pre-create checks...
Creating machine...
(worker1) Copying /Users/ych/.docker/machine/cache/boot2docker.iso to /Users/ych/.docker/machine/machines/worker1/boot2docker.iso...
(worker1) Creating VirtualBox VM...
(worker1) Creating SSH key...
(worker1) Starting the VM...
(worker1) Check network to re-create if needed...
(worker1) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env worker1
☁  ~  docker-machine ssh worker1
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/
 _                 _   ____     _            _
| |__   ___   ___ | |_|___ \ __| | ___   ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__|   <  __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 18.03.1-ce, build HEAD : cb77972 - Thu Apr 26 16:40:36 UTC 2018
Docker version 18.03.1-ce, build 9ee9f40
docker@worker1:~$ docker swarm join --token SWMTKN-1-1aqikkhsz91l4n7k9ig3xinjz0iv0fh4gcrlhp9mk364
3rblca-aqgqldlrw33k8heiao7yx27w5 192.168.99.101:2377
This node joined a swarm as a worker.

我们可以看到上面的提示信息:This node joined a swarm as a worker.,表明节点已经加入到swarm集群之中了。

查看集群

经过上边的两步,我们已经拥有了一个最小的 Swarm 集群,包含一个管理节点和两个工作节点。
管理节点使用docker node ls查看集群:

☁  ~  docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
3gsjpckj5ag1vvdg44fgzylow *   manager             Ready               Active              Leader              18.03.1-ce
cxmj5lr0vbwo1em9y9oang5m8     worker1             Ready               Active                                  18.03.1-ce
ksruum3uc1c265ywm4kn9a88g     worker2             Ready               Active                                  18.03.1-ce
☁  ~  docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
☁  ~  docker service create --replicas 3 -p 80:80 --name nginx nginx:1.13.7-alpine
4k9cbna8ive87p4or9mny9kfs
overall progress: 3 out of 3 tasks
1/3: running   [==================================================>]
2/3: running   [==================================================>]
3/3: running   [==================================================>]
verify: Service converged
☁  ~  docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER        ERRORS
manager   *        virtualbox   Running   tcp://192.168.99.101:2376           v18.03.1-ce
worker1   -        virtualbox   Running   tcp://192.168.99.102:2376           v18.03.1-ce
worker2   -        virtualbox   Running   tcp://192.168.99.103:2376           v18.03.1-ce
☁  ~  docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                 PORTS
4k9cbna8ive8        nginx               replicated          3/3                 nginx:1.13.7-alpine   *:80->80/tcp
☁  ~  docker service ps nginx
ID                  NAME                IMAGE                 NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
r7hmzkqsri8p        nginx.1             nginx:1.13.7-alpine   worker1             Running             Running about a minute ago
y0xgrfwmjfrj        nginx.2             nginx:1.13.7-alpine   worker2             Running             Running about a minute ago
j8k7be8xkbg3        nginx.3             nginx:1.13.7-alpine   manager             Running             Running about a minute ago

使用docker service logs来查看某个服务的日志。

☁  ~  docker service logs nginx

使用docker service rm来从 Swarm 集群移除某个服务:

☁  ~  docker service rm nginx
nginx

正如之前使用docker-compose.yml来一次配置、启动多个容器,在Swarm集群中也可以使用compose文件(docker-compose.yml)来配置、启动多个服务。
上一节中,我们使用docker service create一次只能部署一个服务,使用docker-compose.yml我们可以一次启动多个关联的服务。
我们以在Swarm集群中部署WordPress为例进行说明:(docker-compose.yml)

version: "3"
services:
  wordpress:
    image: wordpress
    ports:
      - 80:80
    networks:
      - overlay
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
    deploy:
      mode: replicated
      replicas: 3
  db:
    image: mysql
    networks:
       - overlay
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
    deploy:
      placement:
        constraints: [node.role == manager]
  visualizer:
    image: dockersamples/visualizer:stable
    ports:
      - "8080:8080"
    stop_grace_period: 1m30s
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]
volumes:
  db-data:
networks:
  overlay:

其中constraints: [node.role == manager]是调度策略,文档地址:https://docs.docker.com/swarm/scheduler/filter/
在 Swarm 集群管理节点新建该文件,其中的 visualizer 服务提供一个可视化页面,我们可以从浏览器中很直观的查看集群中各个服务的运行节点。
在 Swarm 集群中使用 docker-compose.yml 我们用docker stack命令,下面我们对该命令进行详细讲解。

部署服务

部署服务使用docker stack deploy,其中-c参数指定 compose 文件名。

$ docker stack deploy -c docker-compose.yml wordpress

查看服务

$ docker stack ls
NAME                SERVICES
wordpress           3

移除服务

要移除服务,使用docker stack down:

$ docker stack down wordpress
Removing service wordpress_db
Removing service wordpress_visualizer
Removing service wordpress_wordpress
Removing network wordpress_overlay
Removing network wordpress_default

该命令不会移除服务所使用的数据卷,如果你想移除数据卷请使用docker volume rm

11. 图形化管理和监控

下面我们介绍几个可以用图形化的方式来管理Docker的工具。

Shipyard:https://github.com/shipyard/shipyard(已停止维护)

Portainer

Portainer(基于 Go)是一个轻量级的管理界面,可让您轻松管理Docker主机或Swarm集群。
Portainer的使用意图是简单部署。它包含可以在任何 Docker 引擎上运行的单个容器(Docker for Linux 和 Docker for Windows)。
Portainer允许您管理 Docker 容器、image、volume、network 等。 它与独立的 Docker 引擎和 Docker Swarm 兼容。
Docker 命令安装:

$ docker volume create portainer_data
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

Swarm集群部署:

$ docker volume create portainer_data
$ docker service create \
--name portainer \
--publish 9000:9000 \
--replicas=1 \
--constraint 'node.role == manager' \
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \
--mount type=volume,src=portainer_data,dst=/data \
portainer/portainer \
-H unix:///var/run/docker.sock

Docker Compose 部署:

version: '2'
services:
  portainer:
    image: portainer/portainer
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
volumes:
  portainer_data:

docker基础 - 图11portainer

Rancher

Rancher是一个开源的企业级容器管理平台。通过Rancher,企业不必自己使用一系列的开源软件去从头搭建容器服务平台。Rancher提供了在生产环境中使用管理DockerKubernetes的全栈化容器部署与管理平台。docker基础 - 图12
在后面学习kubernetes的课程的时候会给大家演示,用于我们快速搭建一个可运行kubernetes集群环境,非常方便。

cAdvisor

cAdvisorGoogle开发的容器监控工具,我们来看看 cAdvisor 有什么能耐。

  • 监控 Docker Host cAdvisor 会显示当前 host 的资源使用情况,包括 CPU、内存、网络、文件系统等。
  • 监控容器 点击 Docker Containers 链接,显示容器列表。点击某个容器,比如 sysdig,进入该容器的监控页面。

以上就是 cAdvisor 的主要功能,总结起来主要两点:

  • 展示 Host 和容器两个层次的监控数据。
  • 展示历史变化数据。

由于cAdvisor提供的操作界面略显简陋,而且需要在不同页面之间跳转,并且只能监控一个 host,这不免会让人质疑它的实用性。但 cAdvisor 的一个亮点是它可以将监控到的数据导出给第三方工具,由这些工具进一步加工处理。
我们可以把 cAdvisor 定位为一个监控数据收集器,收集和导出数据是它的强项,而非展示数据。 cAdvisor 支持很多第三方工具,其中就包括后面我们重点要学习的Prometheus

$ docker run \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:rw \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --volume=/dev/disk/:/dev/disk:ro \
  --publish=8080:8080 \
  --detach=true \
  --name=cadvisor \
  google/cadvisor:latest

通过访问地址:http://127.0.0.1:8080/containers/ 可以查看所有容器信息: docker基础 - 图13除此之外,cAdvisor 还提供了一个 Rest API:https://github.com/google/cadvisor/blob/master/docs/api.md
cAdvisor 通过该 REST API 暴露监控数据,格式如下:

http://<hostname>:<port>/api/<version>/<request>
  1. Docker 的多阶段构建
    Docker的口号是 Build,Ship,and Run Any App,Anywhere,在我们使用 Docker 的大部分时候,的确能感觉到其优越性,但是往往在我们 Build 一个应用的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语言肯定是不行的,因为实际运行的时候我只需要把最终构建的二进制包给你就行,把源码也一起打包在镜像中,需要承担很多风险,即使是脚本语言,在构建的时候也可能需要使用到一些上线的工具,这样无疑也增大了我们的镜像体积。

示例
比如我们现在有一个最简单的 golang 服务,需要构建一个最小的Docker 镜像,源码如下:

package main
import (
“github.com/gin-gonic/gin”
“net/http”
)
func main() {
router := gin.Default()
router.GET(“/ping”, func(c *gin.Context) {
c.String(http.StatusOK, “PONG”)
})
router.Run(“:8080”)
}
解决方案
我们最终的目的都是将最终的可执行文件放到一个最小的镜像(比如alpine)中去执行,怎样得到最终的编译好的文件呢?基于 Docker 的指导思想,我们需要在一个标准的容器中编译,比如在一个 Ubuntu 镜像中先安装编译的环境,然后编译,最后也在该容器中执行即可。

但是如果我们想把编译后的文件放置到 alpine 镜像中执行呢?我们就得通过上面的 Ubuntu 镜像将编译完成的文件通过 volume 挂载到我们的主机上,然后我们再将这个文件挂载到 alpine 镜像中去。

这种解决方案理论上肯定是可行的,但是这样的话在构建镜像的时候我们就得定义两步了,第一步是先用一个通用的镜像编译镜像,第二步是将编译后的文件复制到 alpine 镜像中执行,而且通用镜像编译后的文件在 alpine 镜像中不一定能执行。

定义编译阶段的 Dockerfile:(保存为Dockerfile.build)

FROM golang
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server
定义alpine镜像:(保存为Dockerfile.old)

FROM alpine:latest
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /root/
COPY app-server .
CMD [“./app-server”]
根据我们的执行步骤,我们还可以简单定义成一个脚本:(保存为build.sh)

!/bin/sh
echo Building cnych/docker-multi-stage-demo:build

docker build -t cnych/docker-multi-stage-demo:build . -f Dockerfile.build

docker create —name extract cnych/docker-multi-stage-demo:build
docker cp extract:/go/src/app/app-server ./app-server
docker rm -f extract

echo Building cnych/docker-multi-stage-demo:old

docker build —no-cache -t cnych/docker-multi-stage-demo:old . -f Dockerfile.old
rm ./app-server
当我们执行完上面的构建脚本后,就实现了我们的目标。

多阶段构建
有没有一种更加简单的方式来实现上面的镜像构建过程呢?Docker 17.05版本以后,官方就提供了一个新的特性:Multi-stage builds(多阶段构建)。 使用多阶段构建,你可以在一个 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并表示开始一个新的构建阶段。你可以很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下你需要的内容即可。

我们可以调整前面一节的 Dockerfile 来使用多阶段构建:(保存为Dockerfile)

FROM golang AS build-env
ADD . /go/src/app
WORKDIR /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server

FROM alpine
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY —from=build-env /go/src/app/app-server /usr/local/bin/app-server
EXPOSE 8080
CMD [ “app-server” ]
现在我们只需要一个Dockerfile文件即可,也不需要拆分构建脚本了,只需要执行 build 命令即可:

$ docker build -t cnych/docker-multi-stage-demo:latest .
默认情况下,构建阶段是没有命令的,我们可以通过它们的索引来引用它们,第一个 FROM 指令从0开始,我们也可以用AS指令为阶段命令,比如我们这里的将第一阶段命名为build-env,然后在其他阶段需要引用的时候使用—from=build-env参数即可。

最后我们简单的运行下该容器测试:

$ docker run —rm -p 8080:8080 cnych/docker-multi-stage-demo:latest
运行成功后,我们可以在浏览器中打开http://127.0.0.1:8080/ping地址,可以看到PONG返回。

现在我们就把两个镜像的文件最终合并到一个镜像里面了。

文章中涉及到代码可以前往 github 查看:https://github.com/cnych/docker-multi-stage-demo

  1. Docker 的多阶段构建
    Docker的口号是 Build,Ship,and Run Any App,Anywhere,在我们使用 Docker 的大部分时候,的确能感觉到其优越性,但是往往在我们 Build 一个应用的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语言肯定是不行的,因为实际运行的时候我只需要把最终构建的二进制包给你就行,把源码也一起打包在镜像中,需要承担很多风险,即使是脚本语言,在构建的时候也可能需要使用到一些上线的工具,这样无疑也增大了我们的镜像体积。

示例
比如我们现在有一个最简单的 golang 服务,需要构建一个最小的Docker 镜像,源码如下:

package main
import (
“github.com/gin-gonic/gin”
“net/http”
)
func main() {
router := gin.Default()
router.GET(“/ping”, func(c *gin.Context) {
c.String(http.StatusOK, “PONG”)
})
router.Run(“:8080”)
}
解决方案
我们最终的目的都是将最终的可执行文件放到一个最小的镜像(比如alpine)中去执行,怎样得到最终的编译好的文件呢?基于 Docker 的指导思想,我们需要在一个标准的容器中编译,比如在一个 Ubuntu 镜像中先安装编译的环境,然后编译,最后也在该容器中执行即可。

但是如果我们想把编译后的文件放置到 alpine 镜像中执行呢?我们就得通过上面的 Ubuntu 镜像将编译完成的文件通过 volume 挂载到我们的主机上,然后我们再将这个文件挂载到 alpine 镜像中去。

这种解决方案理论上肯定是可行的,但是这样的话在构建镜像的时候我们就得定义两步了,第一步是先用一个通用的镜像编译镜像,第二步是将编译后的文件复制到 alpine 镜像中执行,而且通用镜像编译后的文件在 alpine 镜像中不一定能执行。

定义编译阶段的 Dockerfile:(保存为Dockerfile.build)

FROM golang
WORKDIR /go/src/app
ADD . /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server
定义alpine镜像:(保存为Dockerfile.old)

FROM alpine:latest
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /root/
COPY app-server .
CMD [“./app-server”]
根据我们的执行步骤,我们还可以简单定义成一个脚本:(保存为build.sh)

!/bin/sh
echo Building cnych/docker-multi-stage-demo:build

docker build -t cnych/docker-multi-stage-demo:build . -f Dockerfile.build

docker create —name extract cnych/docker-multi-stage-demo:build
docker cp extract:/go/src/app/app-server ./app-server
docker rm -f extract

echo Building cnych/docker-multi-stage-demo:old

docker build —no-cache -t cnych/docker-multi-stage-demo:old . -f Dockerfile.old
rm ./app-server
当我们执行完上面的构建脚本后,就实现了我们的目标。

多阶段构建
有没有一种更加简单的方式来实现上面的镜像构建过程呢?Docker 17.05版本以后,官方就提供了一个新的特性:Multi-stage builds(多阶段构建)。 使用多阶段构建,你可以在一个 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并表示开始一个新的构建阶段。你可以很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下你需要的内容即可。

我们可以调整前面一节的 Dockerfile 来使用多阶段构建:(保存为Dockerfile)

FROM golang AS build-env
ADD . /go/src/app
WORKDIR /go/src/app
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server

FROM alpine
RUN apk add -U tzdata
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY —from=build-env /go/src/app/app-server /usr/local/bin/app-server
EXPOSE 8080
CMD [ “app-server” ]
现在我们只需要一个Dockerfile文件即可,也不需要拆分构建脚本了,只需要执行 build 命令即可:

$ docker build -t cnych/docker-multi-stage-demo:latest .
默认情况下,构建阶段是没有命令的,我们可以通过它们的索引来引用它们,第一个 FROM 指令从0开始,我们也可以用AS指令为阶段命令,比如我们这里的将第一阶段命名为build-env,然后在其他阶段需要引用的时候使用—from=build-env参数即可。

最后我们简单的运行下该容器测试:

$ docker run —rm -p 8080:8080 cnych/docker-multi-stage-demo:latest
运行成功后,我们可以在浏览器中打开http://127.0.0.1:8080/ping地址,可以看到PONG返回。

现在我们就把两个镜像的文件最终合并到一个镜像里面了。

文章中涉及到代码可以前往 github 查看:https://github.com/cnych/docker-multi-stage-demo