从零开始学习 Docker

Docker

这篇文章是我学习 Docker 的记录,大部份内容摘抄自 <<Docker — 从入门到实践>> 一书,并不是本人原创.
学习过程当中整理成适合我本身的笔记,其中也包含了我本身的实践记录.

最近工做中遇到项目部署的问题,由于原先旧项目还须要继续在线服役,因此生产环境的一整套东西一直都停留在很低版本的 CentOS 中,不少时候想扩展或想部署一个新功能由于生产环境的问题而不得不花费更多的时间,有时候还不得不放弃.最要命的是咱们新项目的开发环境是 Windows 环境,并且都是用较新的开发环境;而测试环境却又是较新的 CentOS 环境,致使不少时候在这个环境运行没有问题,在另外一个环境却平白无故出问题,期间为了这些事浪费了不少时间.还好发现有 Docker 可以解决这些头痛的问题,固然 Docker 不仅仅只能解决以上问题,它还有不少强大的功能.接下来就从零开始讲讲 Docker.php

什么是 Docker

Docker 是 Docker 公司的开源项目,使用 Google 公司推出的 Go 语言开发的,并于 2013 年 3 月以 Apache 2.0 受权协议开源,主要项目代码在 GitHub 上进行维护。html

下面的图片比较了 Docker 和传统虚拟化方式的不一样之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操做系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有本身的内核,并且也没有进行硬件虚拟。所以容器要比传统虚拟机更为轻便。
传统虚拟化node

Docker

为何要使用 Docker?

Docker 跟传统的虚拟化方式相比具备如下优点:python

更高效的利用系统资源

因为容器不须要进行硬件虚拟以及运行完整操做系统等额外开销,Docker 对系统资源的利用率更高。不管是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。所以,相比虚拟机技术,一个相同配置的主机,每每能够运行更多数量的应用。mysql

更快速的启动时间

传统的虚拟机技术启动应用服务每每须要数分钟,而 Docker 容器应用,因为直接运行于宿主内核,无需启动完整的操做系统,所以能够作到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。linux

一致的运行环境

开发过程当中一个常见的问题是环境一致性问题。因为开发环境、测试环境、生产环境不一致,致使有些 bug 并未在开发过程当中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题。nginx

持续交付和部署

对开发和运维人员来讲,最但愿的就是一次建立或配置,能够在任意地方正常运行。git

使用 Docker 能够经过定制应用镜像来实现持续集成、持续交付、部署。开发人员能够经过 Dockerfile 来进行镜像构建,并结合 持续集成系统进行集成测试,而运维人员则能够直接在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署。github

并且使用 Dockerfile 使镜像构建透明化,不只仅开发团队能够理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。web

更轻松的迁移

因为 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 能够在不少平台上运行,不管是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。所以用户能够很轻易的将在一个平台上运行的应用,迁移到另外一个平台上,而不用担忧运行环境的变化致使应用没法正常运行的状况。

更轻松的维护和扩展

Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得很是简单。此外,Docker 团队同各个开源项目团队一块儿维护了一大批高质量的官方镜像,既能够直接在生产环境使用,又能够做为基础进一步定制,大大的下降了应用服务的镜像制做成本。

对比传统虚拟机总结

image.png

基本概念

Docker 包括三个基本概念

  • 镜像(Image)
  • 容器(Container)
  • 仓库(Repository)

理解了这三个概念,就理解了 Docker 的整个生命周期。

Docker 镜像

咱们都知道,操做系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像,就至关因而一个 root 文件系统。好比 Docker 官方镜像 ubuntu:14.04 就包含了完整的一套 Ubuntu 14.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建以后也不会被改变。

Docker 容器

镜像和容器的关系,就像是面向对象程序设计中的实例同样,镜像是静态的定义,容器是镜像运行时的实体。容器能够被建立、启动、中止、删除、暂停等。

每个容器运行时,是以镜像为基础层,在其上建立一个当前容器的存储层,咱们能够称这个为容器运行时读写而准备的存储层为容器存储层

容器存储层的生存周期和容器同样,容器消亡时,容器存储层也随之消亡。所以,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不该该向其存储层内写入任何数据,容器存储层要保持无状态化。全部的文件写入操做,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。所以,使用数据卷后,容器能够随意删除、从新 run,数据却不会丢失。

Docker 仓库

镜像构建完成后,能够很容易的在当前宿主上运行,可是,若是须要在其它服务器上使用这个镜像,咱们就须要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

安装 Docker

官方网站上有各类环境下的 安装指南,这里主要介绍下 CentOS 的安装。

CentOS 操做系统安装 Docker

系统要求

Docker 须要安装在 CentOS 7 64 位的平台,而且内核版本不低于 3.10. CentOS 7.× 知足要求的最低内核版本要求,但因为 CentOS 7 内核版本比较低,部分功能(如 overlay2 存储层驱动)没法使用,而且部分功能可能不太稳定。因此建议你们升级到最新的 CentOS 版本,而且内核也更新到最新的稳定版本.更新的方法能够看看个人<<CentOS 7. × 系统及内核升级指南>>

使用阿里云的安装脚本自动安装

为了简化 Docker 安装流程,咱们可使用阿里云提供的一套安装脚本,CentOS 系统上可使用这套脚本安装 Docker :

curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

执行这个命令后,脚本就会自动的将一切准备工做作好,而且把 Docker 安装在系统中。

Docker 经过运行 hello-world 映像验证是否正确安装。

$ docker run hello-world

> Unable to find image 'hello-world:latest' locally
> latest: Pulling from library/hello-world
> b04784fba78d: Pull complete 
> Digest: sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f
> Status: Downloaded newer image for hello-world:latest
 
> Hello from Docker!
> This message shows that your installation appears to be working correctly.

> To generate this message, Docker took the following steps:
>  1. The Docker client contacted the Docker daemon.
>  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
>  3. The Docker daemon created a new container from that image which runs the
>     executable that produces the output you are currently reading.
>  4. The Docker daemon streamed that output to the Docker client, which sent it
>     to your terminal.

> To try something more ambitious, you can run an Ubuntu container with:
>  $ docker run -it ubuntu bash

> Share images, automate workflows, and more with a free Docker ID:
>  https://cloud.docker.com/

> For more examples and ideas, visit:
>  https://docs.docker.com/engine/userguide/

此命令下载测试镜像并在容器中运行它。当容器运行时,它打印一条信息消息并退出。若是你没有配置镜像加速器的话,运行 hello-world 映像验证也是不会成功的.由于国内网络的缘由,没法下载测试镜像,更别说运行测试镜像了,因此这一步能够先跳过,继续往下看,等一下配置完镜像加速器再来验证.

查看当前 Docker 的版本

$ docker -v

> Docker version 17.05.0-ce, build 89658be

能够看出当前的 Docker 为 Docker CE 17.05.0 版本,CE 表明 Docker 社区版,EE 表明 Docker 企业版.

卸载 Docker CE

卸载Docker软件包:

$ yum remove docker-ce

卸载旧版本 Docker

较老版本的 Docker 被称为 docker 或 docker-engine。若是这些已安装,请卸载它们以及关联的依赖关系。

$ yum remove docker docker-common docker-selinux docker-engine

主机上的图像,容器,卷或自定义配置文件不会自动删除。必须手动删除任何已编辑的配置文件。删除全部图像,容器和卷:

$ rm -rf /var/lib/docker

参考文档

参见 Docker 官方 CentOS 安装文档.

镜像加速器

国内访问 Docker Hub 有时会遇到困难,此时能够配置镜像加速器。国内不少云服务商都提供了加速器服务,例如:

注册用户而且申请加速器,会得到如 https://jxus37ad.mirror.aliyuncs.com 这样的地址。咱们须要将其配置给 Docker 引擎。

systemctl enable docker 启用服务后,编辑 /etc/systemd/system/multi-user.target.wants/docker.service 文件,找到 ExecStart= 这一行,在这行最后添加加速器地址 --registry-mirror=<加速器地址>,如:

ExecStart=/usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com

注:对于 1.12 之前的版本,dockerd 换成 docker daemon

从新加载配置而且从新启动。

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

检查加速器是否生效

Linux系统下配置完加速器须要检查是否生效,在命令行执行 ps -ef | grep dockerd,若是从结果中看到了配置的 --registry-mirror 参数说明配置成功。

$ sudo ps -ef | grep dockerd

> root      5346     1  0 19:03 ?        00:00:00 /usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com

使用 Docker 镜像

Docker 运行容器前须要本地存在对应的镜像,若是镜像不存在本地,Docker 会从镜像仓库下载(默认是 Docker Hub 公共注册服务器中的仓库)。

获取镜像

阿里云镜像库 上有大量的高质量的镜像能够用,这里咱们就说一下怎么获取这些镜像并运行。

获取镜像的命令是 docker pull。其命令格式为:

docker pull [选项] [Docker Registry地址]<仓库名>:<标签>

具体的选项能够经过 docker pull --help 命令看到,这里咱们说一下镜像名称的格式。

  • Docker Registry地址:地址的格式通常是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如以前所说,这里的仓库名是两段式名称,既 <用户名>/<软件名>。对于 Docker Hub,若是不给出用户名,则默认为 library,也就是官方镜像.必定要配置镜像加速器,否则下载速度很慢。

好比:

$ docker pull ubuntu:14.04

14.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:14.04

上面的命令中没有给出 Docker Registry 地址,所以将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:14.04,所以将会获取官方镜像 library/ubuntu 仓库中标签为 14.04 的镜像。

查看已下载的镜像

要想列出已经下载下来的镜像,可使用 docker images 命令。

$ docker images

REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hello-world          latest              1815c82652c0        3 weeks ago         1.84kB
ubuntu               14.04               4a2820e686c4        2 weeks ago         188 MB

列表包含了仓库名、标签、镜像 ID、建立时间以及所占用的空间。

运行

有了镜像后,咱们就能够以这个镜像为基础启动一个容器来运行。以上面的 ubuntu:14.04 为例,若是咱们打算启动里面的 bash 而且进行交互式操做的话,能够执行下面的命令。

$ docker run -it --rm ubuntu:14.04 bash

root@e7009c6ce357:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
root@e7009c6ce357:/# exit
exit

docker run 就是运行容器的命令,具体格式咱们会在后面的章节讲解,咱们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操做,一个是 -t 终端。咱们这里打算进入 bash 执行一些命令并查看返回结果,所以咱们须要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认状况下,为了排障需求,退出的容器并不会当即删除,除非手动 docker rm。咱们这里只是随便执行个命令,看看结果,不须要排障和保留结果,所以使用 --rm 能够避免浪费空间。
  • ubuntu:14.04:这是指用 ubuntu:14.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里咱们但愿有个交互式 Shell,所以用的是 bash

进入容器后,咱们能够在 Shell 下操做,执行任何所需的命令。这里,咱们执行了 cat /etc/os-release,这是 Linux 经常使用的查看当前系统版本的命令,从返回的结果能够看到容器内是 Ubuntu 14.04.5 LTS 系统。

最后咱们经过 exit 退出了这个容器。

定制镜像

如今让咱们以定制一个 Web 服务器为例子,来说解镜像是如何构建的。

$ 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 欢迎页面。

Nginx 欢迎页面

如今,改动这个欢迎页面,改为Hello, Docker!,咱们可使用 docker exec 命令进入容器,修改其内容。

$ docker exec -it webserver bash

root@f532879089c6:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@f532879089c6:/# exit
exit

咱们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是得到一个可操做的 Shell。

而后,咱们用 <h1>Hello, Docker!</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。

如今咱们再刷新浏览器的话,会发现内容被改变了。

Nginx 欢迎页面

咱们修改了容器的文件,也就是改动了容器的存储层。咱们能够经过 docker diff 命令看到具体的改动。

$ docker diff webserver

C /root
A /root/.bash_history
C /run
A /run/nginx.pid
C /usr/share/nginx/html/index.html
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

如今已经定制好了,那咱们如何把它保存下来造成镜像?

要知道,当咱们运行一个容器的时候(若是不使用卷的话),咱们作的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,能够将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。之后咱们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。

docker commit 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

咱们能够用下面的命令将容器保存为镜像:

$ docker commit --author "longhui <653155073@qq.com>" --message "修改了Nginx 欢迎页面"  webserver nginx:v2

> sha256:ed889f9d550dd84d81b58eb9e340d49ecbb012b40f5b6507bd388dc335c0d4f5

其中 --author 是指定修改的做者,而 --message 则是记录本次修改的内容。

能够用 docker images 命令看到这个新定制的镜像:

$ docker images

  REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
  nginx               v2                  ed889f9d550d        4 minutes ago       108MB
  nginx               latest              2f7f7bce8929        5 days ago          108MB
  hello-world         latest              1815c82652c0        3 weeks ago         1.84kB

咱们还能够用 docker history 具体查看镜像内的历史记录,若是比较 nginx:latest 的历史记录,咱们会发现新增了咱们刚刚提交的这一层。

$ docker history nginx:v2

  IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
  ed889f9d550d        20 minutes ago      nginx -g daemon off;                            164B                修改了Nginx 欢迎页面
  2f7f7bce8929        5 days ago          /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B
  <missing>           5 days ago          /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B
  <missing>           5 days ago          /bin/sh -c #(nop)  EXPOSE 80/tcp                0B
  <missing>           5 days ago          /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B
  <missing>           5 days ago          /bin/sh -c apt-get update  && apt-get inst...   52.2MB
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.2....   0B
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13....   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
  <missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:54d82a3a8fe8d47...   55.3MB

新的镜像定制好后,咱们能够来运行这个镜像。

docker run --name web2 -d -p 81:80 nginx:v2

这里咱们命名为新的服务为 web2,而且映射到 81 端口。若是是 Docker for Mac/Windows 或 Linux 桌面的话,咱们就能够直接访问 http://localhost:81 看到结果,其内容应该和以前修改后的 webserver 同样。

完成了第一次定制镜像,使用的是 docker commit 命令,手动操做给旧的镜像添加了新的一层,造成新的镜像,对镜像多层存储应该有了更直观的感受。

慎用 docker commit

使用 docker commit 命令虽然能够比较直观的帮助理解镜像分层存储的概念,可是实际环境中并不会这样使用。

首先,若是仔细观察以前的 docker diff webserver 的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,因为命令的执行,还有不少文件被改动或添加了。这还仅仅是最简单的操做,若是是安装软件包、编译构建,那会有大量的无关内容被添加进来,若是不当心清理,将会致使镜像极为臃肿。

此外,使用 docker commit 意味着全部对镜像的操做都是黑箱操做,生成的镜像也被称为黑箱镜像,换句话说,就是除了制做镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。并且,即便是这个制做镜像的人,过一段时间后也没法记清具体在操做的。虽然 docker diff 或许能够告诉获得一些线索,可是远远不到能够确保生成一致镜像的地步。这种黑箱镜像的维护工做是很是痛苦的。

并且,回顾以前说起的镜像所使用的分层存储的概念,除当前层外,以前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。若是使用 docker commit 制做镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即便根本没法访问到。这会让镜像更加臃肿。

docker commit 命令除了学习以外,还有一些特殊的应用场合,好比被入侵后保存现场等。可是,不要使用 docker commit 定制镜像,定制行为应该使用 Dockerfile 来完成。

使用 Dockerfile 定制镜像

从刚才的学习中,咱们能够了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。若是咱们能够把每一层修改、安装、构建、操做的命令都写入一个脚本,用这个脚原本构建、定制镜像,那么以前说起的没法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,所以每一条指令的内容,就是描述该层应当如何构建。

还以以前定制 nginx 镜像为例,此次咱们使用 Dockerfile 来定制。

在一个空白目录中,创建一个文本文件,并命名为 Dockerfile

$ mkdir mynginx
$ cd mynginx/
$ touch Dockerfile

添加如下内容:

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

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

FROM 指定基础镜像

所谓定制镜像,那必定是以一个镜像为基础,在其上进行定制。就像咱们以前运行了一个 nginx 镜像的容器,再进行修改同样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,所以一个 DockerfileFROM 是必备的指令,而且必须是第一条指令。

RUN 执行命令

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

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令同样。刚才写的 Dockrfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本同样能够执行命令,那么咱们是否就能够像 Shell 脚本同样把每一层构建须要的命令写出来,好比这样:

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。在撰写 Dockerfile 的时候,要常常提醒本身,这并非在写 Shell 脚本,而是在定义每一层该如何构建。

而且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,好比换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还能够看到这一组命令的最后添加了清理工做的命令,删除了为了编译构建所须要的软件,清理了全部下载、展开的文件,而且还清理了 apt 缓存文件。这是很重要的一步,咱们以前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。所以镜像构建时,必定要确保每一层只添加真正须要添加的东西,任何无关的东西都应该清理掉。

不少人初学 Docker 制做出了很臃肿的镜像的缘由之一,就是忘记了每一层构建的最后必定要清理掉无关文件。

构建镜像

再回到以前定制的 nginx 镜像的 Dockerfile 来。如今咱们明白了这个 Dockerfile 的内容,那么让咱们来构建这个镜像吧。

Dockerfile 文件所在目录执行:

$ docker build -t nginx:v3 .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 2f7f7bce8929
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in f3f1e0d41576
 ---> e189d22f23b5
Removing intermediate container f3f1e0d41576
Successfully built e189d22f23b5
Successfully tagged nginx:v3

从命令的输出结果中,咱们能够清晰的看到镜像的构建过程。在 Step 2/2 中,如同咱们以前所说的那样,RUN 指令启动了一个容器 f3f1e0d41576,执行了所要求的命令,并最后提交了这一层 e189d22f23b5,随后删除了所用到的这个容器 f3f1e0d41576

这里咱们使用了 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,以及会将其置于镜像构建上下文目录中。

Dockerfile 指令详解

COPY 复制文件

格式:

  • COPY <源路径>... <目标路径>
  • COPY ["<源路径1>",... "<目标路径>"]

RUN 指令同样,也有两种格式,一种相似于命令行,一种相似于函数调用。

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。好比:

COPY package.json /usr/src/app/

<源路径> 能够是多个,甚至能够是通配符,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 能够是容器内的绝对路径,也能够是相对于工做目录的相对路径(工做目录能够用 WORKDIR 指令来指定)。目标路径不须要事先建立,若是目录不存在会在复制文件前先行建立缺失目录。

此外,还须要注意一点,使用 COPY 指令,源文件的各类元数据都会保留。好比读、写、执行权限、文件变动时间等。这个特性对于镜像定制颇有用。特别是构建相关文件都在使用 Git 进行管理的时候。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。可是在 COPY 基础上增长了一些功能。

好比 <源路径> 能够是一个 URL,这种状况下,Docker 引擎会试图去下载这个连接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,若是这并非想要的权限,那么还须要增长额外的一层 RUN 进行权限调整,另外,若是下载的是个压缩包,须要解压缩,也同样还须要额外的一层 RUN 指令进行解压缩。因此不如直接使用 RUN 指令,而后使用 wget 或者 curl 工具下载,处理权限、解压缩、而后清理无用文件更合理。所以,这个功能其实并不实用,并且不推荐使用。

若是 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的状况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

在某些状况下,这个自动解压缩的功能很是有用,好比官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些状况下,若是咱们真的是但愿复制个压缩文件进去,而不解压缩,这时就不可使用 ADD 命令了。

在 Docker 官方的最佳实践文档中要求,尽量的使用 COPY,由于 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不必定很清晰。最适合使用 ADD 的场合,就是所说起的须要自动解压缩的场合。

另外须要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

所以在 COPYADD 指令中选择的时候,能够遵循这样的原则,全部的文件复制均使用 COPY 指令,仅在须要自动解压缩的场合使用 ADD

CMD 容器启动命令

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,须要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

CMD 指令的格式和 RUN 类似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

在运行时能够指定新的命令来替代镜像设置中的这个默认命令,好比,ubuntu 镜像默认的 CMD/bin/bash,若是咱们直接 docker run -it ubuntu 的话,会直接进入 bash。咱们也能够在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,通常推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,所以必定要使用双引号 ",而不要使用单引号。

若是使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。好比:

CMD echo $HOME

在实际执行中,会将其变动为:

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

这就是为何咱们可使用环境变量的缘由,由于这些环境变量会被 shell 进行解析处理。

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该之前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将 CMD 写为:

CMD service nginx start

而后发现容器执行后就当即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是由于没有搞明白前台、后台的概念,没有区分容器和虚拟机的差别,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它须要关心的东西。

而使用 service nginx start 命令,则是但愿 upstart 来之后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],所以主进程其实是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 做为主进程退出了,天然就会令容器退出。

正确的作法是直接执行 nginx 可执行文件,而且要求之前台形式运行。好比:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式同样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 同样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也能够替代,不过比 CMD 要略显繁琐,须要经过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,再也不是直接的运行其命令,而是将 CMD 的内容做为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了 CMD 后,为何还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让咱们来看几个场景。

场景一:让镜像变成像命令同样使用

假设咱们须要一个得知本身当前公网 IP 的镜像,那么能够先用 CMD 来实现:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]

假如咱们使用 docker build -t myip . 来构建镜像的话,若是咱们须要查询当前公网 IP,只须要执行:

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

嗯,这么看起来好像能够直接把镜像当作命令使用了,不过命令总有参数,若是咱们但愿加参数呢?好比从上面的 CMD 中能够看到实质的命令是 curl,那么若是咱们但愿显示 HTTP 头信息,就须要加上 -i 参数。那么咱们能够直接加 -i 参数给 docker run myip 么?

$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

咱们能够看到可执行文件找不到的报错,executable file not found。以前咱们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。所以这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,因此天然找不到。

那么若是咱们但愿加入 -i 这参数,咱们就必须从新完整的输入这个命令:

$ docker run myip curl -s http://ip.cn -i

这显然不是很好的解决方案,而使用 ENTRYPOINT 就能够解决这个问题。如今咱们从新用 ENTRYPOINT 来实现这个镜像:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

此次咱们再来尝试直接使用 docker run myip -i

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

能够看到,此次成功了。这是由于当存在 ENTRYPOINT 后,CMD 的内容将会做为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,所以会做为参数传给 curl,从而达到了咱们预期的效果。

场景二:应用运行前的准备工做

启动容器就是启动主进程,但有些时候,启动主进程前,须要一些准备工做。

好比 mysql 类的数据库,可能须要一些数据库配置、初始化的工做,这些工做要在最终的 mysql 服务器运行以前解决。

此外,可能但愿避免使用 root 用户去启动服务,从而提升安全性,而在启动服务前还须要以 root 身份执行一些必要的准备工做,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可使用 root 身份执行,方便调试等。

这些准备工做是和容器 CMD 无关的,不管 CMD 为何,都须要事先进行一个预处理的工做。这种状况下,能够写一个脚本,而后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)做为命令,在脚本最后执行。好比官方镜像 redis 中就是这么作的:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

能够看到其中为了 redis 服务建立了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    chown -R redis .
    exec su-exec redis "$0" "$@"
fi

exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,若是是 redis-server 的话,则切换到 redis 用户身份启动服务器,不然依旧使用 root 身份执行。好比:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,不管是后面的其它指令,如 RUN,仍是运行时的应用,均可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可使用这个环境变量。好比在官方 node 镜像 Dockerfile 中,就有相似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,屡次使用 $NODE_VERSION 来进行操做定制。能够看到,未来升级镜像构建版本的时候,只须要更新 7.2.0 便可,Dockerfile 构建维护变得更轻松了。

下列指令能够支持环境变量引用: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

能够从这个指令列表里感受到,环境变量可使用的地方不少,很强大。经过环境变量,咱们可让一份 Dockerfile 制做更多的镜像,只需使用不一样的环境变量便可。

ARG 构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果同样,都是设置环境变量。所不一样的是,ARG 所设置的构建环境的环境变量,在未来容器运行时是不会存在这些环境变量的。可是不要所以就使用 ARG 保存密码之类的信息,由于 docker history 仍是能够看到全部值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值能够在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

在 1.13 以前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。若是对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,再也不报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用一样的构建流程构建不一样的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每一个 Dockerfile 的内容修改。

VOLUME 定义匿名卷

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

以前说过,容器运行时应该尽可能保持容器存储层不发生写操做,对于数据库类须要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节咱们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,咱们能够事先指定某些目录挂载为匿名卷,这样在运行时若是用户不指定挂载,其应用也能够正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。固然,运行时能够覆盖这个挂载设置。好比:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会由于这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另外一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工做目录

格式为 WORKDIR <工做目录路径>

使用 WORKDIR 指令能够来指定工做目录(或者称为当前目录),之后各层的当前目录就被改成指定的目录,如该目录不存在,WORKDIR 会帮你创建目录。

以前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚原本书写,这种错误的理解还可能会致使出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

若是将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。缘由其实很简单,在 Shell 中,连续两行是同一个进程执行环境,所以前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不一样,是两个彻底不一样的容器。这就是对 Dokerfile 构建分层存储的概念不了解所致使的错误。

以前说过每个 RUN 都是启动一个容器、执行命令、而后提交存储层文件变动。第一层 RUN cd /app 的执行仅仅是当前进程的工做目录变动,一个内存上的变化而已,其结果不会形成任何文件变动。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更彻底不要紧,天然不可能继承前一层构建过程当中的内存变化。

所以若是须要改变之后各层的工做目录的位置,那么应该使用 WORKDIR 指令。

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康情况的命令
  • HEALTHCHECK NONE:若是基础镜像有健康检查指令,使用这行能够屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常.

在没有 HEALTHCHECK 指令前,Docker 引擎只能够经过容器内主进程是否退出来判断容器是否状态异常。不少状况下这没问题,可是若是程序进入死锁状态,或者死循环状态,应用进程并不退出,可是该容器已经没法提供服务了。在 1.12 之前,Docker 不会检测到容器的这种状态,从而不会从新调度,致使可能会有部分容器已经没法提供服务了却还在接受用户请求。

而自 1.12 以后,Docker 提供了 HEALTHCHECK 指令,经过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,若是连续必定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,若是超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

CMD, ENTRYPOINT 同样,HEALTHCHECK 只能够出现一次,若是写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 同样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假设咱们有个镜像是个最简单的 Web 服务,咱们但愿增长健康检查来判断其 Web 服务是否在正常工做,咱们能够用 curl 来帮助判断,其 DockerfileHEALTHCHECK 能够这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

这里咱们设置了每 5 秒检查一次(这里为了试验因此间隔很是短,实际应该相对较长),若是健康检查命令超过 3 秒没响应就视为失败,而且使用 curl -fs http://localhost/ || exit 1 做为健康检查命令。

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,咱们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,能够经过 docker ps 看到最初的状态为 (health: starting)

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次 docker ps,就会看到健康状态变化为了 (healthy)

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

若是健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,能够用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD 镜像复用及项目环境管理

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,好比 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,惟有 ONBUILD 是为了帮助别人定制本身而准备的。

假设咱们要制做 Node.js 所写的应用的镜像。咱们都知道 Node.js 使用 npm 进行包管理,全部依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,须要先进行 npm install 才能够得到全部须要的依赖。而后就能够经过 npm start 来启动应用。所以,通常来讲会这样写 Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就能够直接拿来启动容器运行。可是若是咱们还有第二个 Node.js 项目也差很少呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那若是有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让咱们继续看这样的场景维护的问题。

若是第一个 Node.js 项目在开发过程当中,发现这个 Dockerfile 里存在问题,好比敲错字了、或者须要安装额外的包,而后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,可是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,可是并不会由于第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。

那么咱们可不能够作一个基础镜像,而后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,从新构建后就继承了基础镜像的更新?好吧,能够,让咱们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

这里咱们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的本身的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 从新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。若是这个 Dockerfile 里面有些东西须要调整呢?好比 npm install 都须要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,由于涉及到了当前项目的 ./package.json,难道又要一个个修改么?因此说,这样制做基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,然后面三条指令的变化则彻底没办法处理。

ONBUILD 能够解决这个问题。让咱们用 ONBUILD 从新写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

此次咱们回到原始的 Dockerfile,可是此次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。而后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,以前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、而且针对本项目执行 npm install,生成应用镜像。

删除本地镜像

若是要删除本地的镜像,可使用 docker rmi 命令,其格式为:

docker rmi [选项] <镜像1> [<镜像2> ...]

注意 docker rm 命令是删除容器,不要混淆。

用 ID、镜像名、摘要删除镜像

其中,<镜像> 能够是 镜像短 ID镜像长 ID镜像名 或者 镜像摘要

好比咱们有这么一些镜像:

$ docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB

咱们能够用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,可是人工输入就太累了,因此更多的时候是用 短 ID 来删除镜像。docker images 默认列出的就已是短 ID 了,通常取前3个字符以上,只要足够区分于别的镜像就能够了。

好比这里,若是咱们要删除 redis:alpine 镜像,能够执行:

$ docker rmi 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7

咱们也能够用镜像名,也就是 <仓库名>:<标签>,来删除镜像。

$ docker rmi centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

固然,更精确的是使用 镜像摘要 删除镜像。

$ docker images --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker rmi node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

用 docker images 命令来配合

像其它能够承接多个实体的命令同样,可使用 docker images -q 来配合使用 docker rmi,这样能够成批的删除但愿删除的镜像。好比以前咱们介绍过的,删除虚悬镜像的指令是:

$ docker rmi $(docker images -q -f dangling=true)

咱们在“镜像列表”章节介绍过不少过滤镜像列表的方式均可以拿过来使用。

好比,咱们须要删除全部仓库名为 redis 的镜像:

$ docker rmi $(docker images -q redis)

或者删除全部在 mongo:3.2 以前的镜像:

$ docker rmi $(docker images -q -f before=mongo:3.2)

充分利用你的想象力和 Linux 命令行的强大,你能够完成不少很是赞的功能。

参考文档

操做 Docker 容器

容器是 Docker 又一核心概念。

简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机能够理解为模拟运行的一整套操做系统(提供了运行态环境和其余系统环境)和跑在上面的应用。

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另一个是将在终止状态(stopped)的容器从新启动。

由于 Docker 的容器实在过轻量级了,不少时候用户都是随时删除和新建立容器。

新建并启动

所须要的命令主要为 docker run

例如,下面的命令输出一个 “Hello World”,以后终止容器。

$ docker run ubuntu:14.04 /bin/echo 'Hello world'

Unable to find image 'ubuntu:14.04' locally
14.04: Pulling from library/ubuntu
cb56c90f0b30: Pull complete
0acc551e5716: Pull complete
8956dcd35143: Pull complete
908242721214: Pull complete
b44ff14dd3bb: Pull complete
Digest: sha256:5faf6cb681da2be979a177b60d8c18497f962e3d82268c49db6c74008d0c294d
Status: Downloaded newer image for ubuntu:14.04
Hello world

这跟在本地直接执行 /bin/echo 'hello world' 几乎感受不出任何区别。

下面的命令则启动一个 bash 终端,容许用户进行交互。

$ docker run -t -i ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#

其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。

在交互模式下,用户能够经过所建立的终端来输入命令,例如

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

容器的核心为所执行的应用程序,所须要的资源都是应用程序运行所必需的。除此以外,并无其它的资源。能够在伪终端中利用 pstop 来查看进程信息。

root@ba267838cc1b:/# ps
  PID TTY          TIME CMD
    1 ?        00:00:00 bash
   11 ?        00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特色使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

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

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

查看正在运行中的容器

利用 docker ps 命令能够查看正在运行中的容器

$ docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:81->80/tcp   webserver

查看全部容器

利用 docker ps -a 命令能够查看全部容器

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   25 minutes ago      Exited (0) 25 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                          peaceful_brown

启动已终止的容器

能够利用 docker start 命令和上面使用 docker ps -a 查看到的 CONTAINER IDNAMES,直接将一个已经终止的容器启动运行。

$ docker start relaxed_kilby

relaxed_kilby

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   45 minutes ago      Exited (0) 3 seconds ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                         peaceful_brown

这里把 新建并启动 章节中的容器又启动了一次,此次这个容器和以前不同,他启动以后就会被终止,不会输出一个 “Hello World”,以后才终止容器。能够看 STATUS 输出,这个容器的确被启动过.

容器后台运行

更多的时候,须要让 Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,能够经过添加 -d 参数来实现。

下面举两个例子来讲明一下。

若是不使用 -d 参数运行容器。

$ sudo docker run ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

容器会把输出的结果(STDOUT)打印到宿主机上面

若是使用了 -d 参数运行容器。

$ sudo docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果(STDOUT)打印到宿主机上面(输出结果能够用docker logs 查看)。

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

使用 -d 参数启动后会返回一个惟一的 id,也能够经过 docker ps 命令来查看容器信息。

$ sudo docker ps
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
77b2dc01fe0f  ubuntu:14.04  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

要获取容器的输出信息,能够经过 docker logs 命令。

$ sudo docker logs [container ID or NAMES]
hello world
hello world
hello world
. . .

终止容器

可使用 docker stop 命令和上面使用的 docker ps -a 查看到的 CONTAINER IDNAMES,来终止一个运行中的容器。

$ docker stop web2

web2

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   About an hour ago   Exited (0) 15 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Exited (0) 3 seconds ago                         web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 2 days ago          Exited (0) 2 days ago                            peaceful_brown

此外,当 Docker 容器中指定的应用终结时,容器也自动终止。例如启动了一个终端的容器,用户经过 exit 命令或 Ctrl+d 来退出终端时,所建立的容器马上终止。

重启容器

docker restart 命令会将一个运行态的容器终止,而后再从新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。
某些时候须要进入容器进行操做,有不少种方法,包括使用 docker attach 命令或 nsenter 工具等。

attach 命令

docker attach 是Docker自带的命令。下面示例如何使用该命令。

$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia
$sudo docker attach nostalgic_hypatia
root@243c32535da7:/#

可是使用 attach 命令有时候并不方便。当多个窗口同时 attach 到同一个容器的时候,全部窗口都会同步显示。当某个窗口因命令阻塞时,其余窗口也没法执行操做了。

nsenter 命令

安装

nsenter 工具在 util-linux 包2.23版本后包含。
可使用 nsenter -V 查看系统是否安装了 nsenter 工具.

$ nsenter -V

nsenter from util-linux 2.23.2

若是系统中 util-linux 包没有该命令,能够按照下面的方法从源码安装。

$ cd /tmp; curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-; cd util-linux-2.24;
$ ./configure --without-ncurses
$ make nsenter && sudo cp nsenter /usr/local/bin

使用

nsenter 启动一个新的shell进程(默认是/bin/bash), 同时会把这个新进程切换到和目标(target)进程相同的命名空间,这样就至关于进入了容器内部。nsenter 要正常工做须要有 root 权限。

为了链接到容器,你还须要找到容器的第一个进程的 PID,能够经过下面的命令获取。

PID=$(docker inspect --format "{{ .State.Pid }}" <container>)

经过这个 PID,就能够链接到这个容器:

$ nsenter --target $PID --mount --uts --ipc --net --pid

若是没法经过以上命令链接到这个容器,有多是由于宿主的默认 shell 在容器中并不存在,好比zsh,可使用以下命令显式地使用bash。

$ nsenter --target $pid --mount --uts --ipc --net --pid  -- /usr/bin/env \ 
--ignore-environment HOME=/root /bin/bash --login

下面给出一个完整的例子。

$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia
$ PID=$(docker-pid 243c32535da7)
10981
$ sudo nsenter --target 10981 --mount --uts --ipc --net --pid
root@243c32535da7:/#

更简单的,建议你们下载
.bashrc_docker,并将内容放到 .bashrc 中。

$ wget -P ~ https://github.com/yeasy/docker_practice/raw/master/_local/.bashrc_docker;
$ echo "[ -f ~/.bashrc_docker ] && . ~/.bashrc_docker" >> ~/.bashrc; source ~/.bashrc

这个文件中定义了不少方便使用 Docker 的命令,例如 docker-pid 能够获取某个容器的 PID;而 docker-enter 能够进入容器或直接在容器内执行命令。

$ echo $(docker-pid <container>)
$ docker-enter <container> ls

导出和导入容器快照

导出容器快照

若是要导出本地某个容器,可使用 docker export 命令。

$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:14.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test
$ sudo docker export 7691a814370e > ubuntu.tar

这样将导出容器快照到本地文件。

导入容器快照

可使用 docker import 从容器快照文件中再导入为镜像,例如

$ cat ubuntu.tar | sudo docker import - test/ubuntu:v1.0
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

此外,也能够经过指定 URL 或者某个目录来导入,例如

$sudo docker import http://example.com/exampleimage.tgz example/imagerepo

*注:用户既可使用 docker load 来导入镜像存储文件到本地镜像库,也可使用 docker import 来导入一个容器快照到本地镜像库。这二者的区别在于容器快照文件将丢弃全部的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时能够从新指定标签等元数据信息。

删除容器

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

$sudo docker rm  trusting_newton
trusting_newton

若是要删除一个运行中的容器,能够添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

清理全部处于终止状态的容器(不建议使用)

docker ps -a 命令能够查看全部已经建立的包括终止状态的容器,若是数量太多要一个个删除可能会很麻烦,用 docker rm $(docker ps -a -q) 能够所有清理掉。

*注意:这个命令其实会试图删除全部的包括还在运行中的容器,不过就像上面提过的 docker rm 默认并不会删除运行中的容器。

私有仓库

有时候使用阿里云这样的公共仓库可能不方便,用户能够建立一个本地仓库供本身使用。

如何使用本地仓库。

docker-registry 是官方提供的工具,能够用于构建私有的镜像仓库。

安装运行 docker-registry

容器中运行 docker-registry

在安装了 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=AKIAHSHB43HS3J92MXZ \
         -e AWS_SECRET=xdDowwlK7TJajV1Y7EoOZrmuPEJlHYcNP2k4j49T \
         -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_REGISTRY_CONFIG=/registry-conf/config.yml registry

默认状况下,仓库会被建立在容器的 /var/lib/registry (v1 中是/tmp/registry)下。能够经过 -v 参数来将镜像文件存放在本地的指定路径。
例以下面的例子将上传的镜像放到 /opt/data/registry 目录。

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

本地安装 docker-registry

对于 CentOS 发行版,能够直接经过源安装。

$ sudo yum install -y python-devel libevent-devel python-pip gcc xz-devel
$ sudo python-pip install docker-registry

也能够从 docker-registry 项目下载源码进行安装。

$ sudo apt-get install build-essential python-dev libevent-dev python-pip libssl-dev liblzma-dev libffi-dev
$ git clone https://github.com/docker/docker-registry.git
$ cd docker-registry
$ sudo python setup.py install

而后修改配置文件,主要修改 dev 模板段的 storage_path 到本地的存储仓库的路径。

$ cp config/config_sample.yml config/config.yml

以后启动 Web 服务。

$ sudo gunicorn -c contrib/gunicorn.py docker_registry.wsgi:application

或者

$ sudo gunicorn --access-logfile - --error-logfile - -k gevent -b 0.0.0.0:5000 -w 4 --max-requests 100 docker_registry.wsgi:application

此时使用 curl 访问本地的 5000 端口,看到输出 docker-registry 的版本信息说明运行成功。

*注:config/config_sample.yml 文件是示例配置文件。

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

建立好私有仓库以后,就可使用 docker tag 来标记一个镜像,而后推送它到仓库,别的机器上就能够下载下来了。例如私有仓库地址为 192.168.7.26:5000

先在本机查看已有的镜像。

$ sudo docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB

使用docker tagba58 这个镜像标记为 192.168.7.26:5000/test(格式为 docker tag IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG])。

$ sudo docker tag ba58 192.168.7.26:5000/test
root ~ # docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
192.168.7.26:5000/test            latest              ba5877dc9bec        6 weeks ago         192.7 MB

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

$ sudo docker push 192.168.7.26:5000/test
The push refers to a repository [192.168.7.26:5000/test] (len: 1)
Sending image list
Pushing repository 192.168.7.26:5000/test (1 tags)
Image 511136ea3c5a already pushed, skipping
Image 9bad880da3d2 already pushed, skipping
Image 25f11f5fb0cb already pushed, skipping
Image ebc34468f71d already pushed, skipping
Image 2318d26665ef already pushed, skipping
Image ba5877dc9bec already pushed, skipping
Pushing tag for rev [ba5877dc9bec] on {http://192.168.7.26:5000/v1/repositories/test/tags/latest}

用 curl 查看仓库中的镜像。

$ curl http://192.168.7.26:5000/v1/search
{"num_results": 7, "query": "", "results": [{"description": "", "name": "library/miaxis_j2ee"}, {"description": "", "name": "library/tomcat"}, {"description": "", "name": "library/ubuntu"}, {"description": "", "name": "library/ubuntu_office"}, {"description": "", "name": "library/desktop_ubu"}, {"description": "", "name": "dockerfile/ubuntu"}, {"description": "", "name": "library/test"}]}

这里能够看到 {"description": "", "name": "library/test"},代表镜像已经被成功上传了。

如今能够到另一台机器去下载这个镜像。

$ sudo docker pull 192.168.7.26:5000/test
Pulling repository 192.168.7.26:5000/test
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete
$ sudo docker images
REPOSITORY                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
192.168.7.26:5000/test             latest              ba5877dc9bec        6 weeks ago         192.7 MB

可使用 这个脚本 批量上传本地的镜像到注册服务器中,默认是本地注册服务器 127.0.0.1:5000。例如:

$ wget https://github.com/yeasy/docker_practice/raw/master/_local/push_images.sh; sudo chmod a+x push_images.sh
$ ./push_images.sh ubuntu:latest centos:centos7
The registry server is 127.0.0.1
Uploading ubuntu:latest...
The push refers to a repository [127.0.0.1:5000/ubuntu] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/ubuntu (1 tags)
Image 511136ea3c5a already pushed, skipping
Image bfb8b5a2ad34 already pushed, skipping
Image c1f3bdbd8355 already pushed, skipping
Image 897578f527ae already pushed, skipping
Image 9387bcc9826e already pushed, skipping
Image 809ed259f845 already pushed, skipping
Image 96864a7d2df3 already pushed, skipping
Pushing tag for rev [96864a7d2df3] on {http://127.0.0.1:5000/v1/repositories/ubuntu/tags/latest}
Untagged: 127.0.0.1:5000/ubuntu:latest
Done
Uploading centos:centos7...
The push refers to a repository [127.0.0.1:5000/centos] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/centos (1 tags)
Image 511136ea3c5a already pushed, skipping
34e94e67e63a: Image successfully pushed
70214e5d0a90: Image successfully pushed
Pushing tag for rev [70214e5d0a90] on {http://127.0.0.1:5000/v1/repositories/centos/tags/centos7}
Untagged: 127.0.0.1:5000/centos:centos7
Done

Docker 数据管理

数据卷

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

  • 数据卷能够在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新,不会影响镜像
  • 数据卷默认会一直存在,即便容器被删除

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

建立一个数据卷

在用 docker run 命令的时候,使用 -v 标记来建立一个数据卷并挂载到容器里。在一次 run 中屡次使用能够挂载多个数据卷。

下面建立一个名为 web 的容器,并加载一个数据卷到容器的 /webapp 目录。

$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py

*注意:也能够在 Dockerfile 中使用 VOLUME 来添加一个或者多个新的卷到由该镜像建立的任意容器。

删除数据卷

数据卷是被设计用来持久化数据的,它的生命周期独立于容器,Docker不会在容器被删除后自动删除数据卷,而且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。若是须要在删除容器的同时移除数据卷。能够在删除容器的时候使用 docker rm -v 这个命令。无主的数据卷可能会占据不少空间,要清理会很麻烦。Docker官方正在试图解决这个问题,相关工做的进度能够查看这个PR

挂载一个主机目录做为数据卷

使用 -v 标记也能够指定挂载一个本地主机的目录到容器中去。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

上面的命令加载主机的 /src/webapp 目录到容器的 /opt/webapp
目录。这个功能在进行测试的时候十分方便,好比用户能够放置一些程序到本地目录中,来查看容器是否正常工做。本地目录的路径必须是绝对路径,若是目录不存在 Docker 会自动为你建立它。

*注意:Dockerfile 中不支持这种用法,这是由于 Dockerfile 是为了移植和分享用的。然而,不一样操做系统的路径格式不同,因此目前还不能支持。

Docker 挂载数据卷的默认权限是读写,用户也能够经过 :ro 指定为只读。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro
training/webapp python app.py

加了 :ro 以后,就挂载为只读了。

查看数据卷的具体信息

在主机里使用如下命令能够查看指定容器的信息

$ docker inspect web
...

在输出的内容中找到其中和数据卷相关的部分,能够看到全部的数据卷都是建立在主机的/var/lib/docker/volumes/下面的

"Volumes": {
    "/webapp": "/var/lib/docker/volumes/fac362...80535"
},
"VolumesRW": {
    "/webapp": true
}
...

注:从Docker 1.8.0起,数据卷配置在"Mounts"Key下面,能够看到全部的数据卷都是建立在主机的/mnt/sda1/var/lib/docker/volumes/....下面了。

"Mounts": [
            {
                "Name": "b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29",
                "Source": "/mnt/sda1/var/lib/docker/volumes/b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29/_data",
                "Destination": "/webapp",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ]
...

挂载一个本地主机文件做为数据卷

-v 标记也能够从主机挂载单个文件到容器中

$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

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

*注意:若是直接挂载一个文件,不少文件编辑工具,包括 vi 或者 sed --in-place,可能会形成文件 inode 的改变,从 Docker 1.1
.0起,这会致使报错误信息。因此最简单的办法就直接挂载文件的父目录。

数据卷容器

若是你有一些持续更新的数据须要在容器之间共享,最好建立数据卷容器。

数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的。

首先,建立一个名为 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 命令来指定同时删除关联的容器。
这可让用户在容器之间升级和移动数据卷。

利用数据卷容器来备份、恢复、迁移数据卷

能够利用数据卷对其中的数据进行进行备份、恢复和迁移。

备份

首先使用 --volumes-from 标记来建立一个加载 dbdata 容器卷的容器,并从主机挂载当前目录到容器的 /backup 目录。命令以下:

$ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器启动后,使用了 tar 命令来将 dbdata 卷备份为容器中 /backup/backup.tar 文件,也就是主机当前目录下的名为 backup.tar 的文件。

恢复

若是要恢复数据到一个容器,首先建立一个带有空数据卷的容器 dbdata2。

$ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

而后建立另外一个容器,挂载 dbdata2 容器卷中的数据卷,并使用 untar 解压备份文件到挂载的容器卷中。

$ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf
/backup/backup.tar

为了查看/验证恢复的数据,能够再启动一个容器挂载一样的容器卷来查看

$ sudo docker run --volumes-from dbdata2 busybox /bin/ls /dbdata

 Docker 中的网络功能介绍

Docker 容许经过外部访问容器或容器互联的方式来提供网络服务。

外部访问容器

容器中能够运行一些网络应用,要让外部也能够访问这些应用,能够经过 -P-p 参数来指定端口映射。

当使用 -P 标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。

使用 docker ps 能够看到,本地主机的 49155 被映射到了容器的 5000 端口。此时访问本机的 49155 端口便可访问容器内 web 应用提供的界面。

$ sudo docker run -d -P training/webapp python app.py
$ sudo docker ps -l
CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES
bc533791f3f5  training/webapp:latest  python app.py 5 seconds ago  Up 2 seconds  0.0.0.0:49155->5000/tcp  nostalgic_morse

一样的,能够经过 docker logs 命令来查看应用的信息。

$ sudo docker logs -f nostalgic_morse
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p(小写的)则能够指定要映射的端口,而且,在一个指定端口上只能够绑定一个容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

映射全部接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,能够执行

$ sudo docker run -d -p 5000:5000 training/webapp python app.py

此时默认会绑定本地全部接口上的全部地址。

映射到指定地址的指定端口

可使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址,好比 localhost 地址 127.0.0.1

$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

映射到指定地址的任意端口

使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口,本地主机会自动分配一个端口。

$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py

还可使用 udp 标记来指定 udp 端口

$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射端口配置

使用 docker port 来查看当前映射的端口配置,也能够查看到绑定的地址

$ docker port nostalgic_morse 5000
127.0.0.1:49155.

注意:

  • 容器有本身的内部网络和 ip 地址(使用 docker inspect 能够获取全部的变量,Docker 还能够有一个可变的网络配置。)
  • -p 标记能够屡次使用来绑定多个端口

例如

$ sudo docker run -d -p 5000:5000  -p 3000:80 training/webapp python app.py

容器互联

容器的链接(linking)系统是除了端口映射外,另外一种跟容器中应用交互的方式。

该系统会在源和接收容器之间建立一个隧道,接收容器能够看到源容器指定的信息。

自定义容器命名

链接系统依据容器的名称来执行。所以,首先须要自定义一个好记的容器命名。

虽然当建立容器的时候,系统默认会分配一个名字。自定义命名容器有2个好处:

  • 自定义的命名,比较好记,好比一个web应用容器咱们能够给它起名叫web
  • 当要链接其余容器时候,能够做为一个有用的参考点,好比链接web容器到db容器

使用 --name 标记能够为容器自定义命名。

$ sudo docker run -d -P --name web training/webapp python app.py

使用 docker ps 来验证设定的命名。

$ sudo docker ps -l
CONTAINER ID  IMAGE                  COMMAND        CREATED       STATUS       PORTS                    NAMES
aed84ee21bde  training/webapp:latest python app.py  12 hours ago  Up 2 seconds 0.0.0.0:49154->5000/tcp  web

也可使用 docker inspect 来查看容器的名字

$ sudo docker inspect -f "{{ .Name }}" aed84ee21bde
/web

注意:容器的名称是惟一的。若是已经命名了一个叫 web 的容器,当你要再次使用 web 这个名称的时候,须要先用docker rm 来删除以前建立的同名容器。

在执行 docker run 的时候若是添加 --rm 标记,则容器在终止后会马上删除。注意,--rm-d 参数不能同时使用。

容器互联

使用 --link 参数可让容器之间安全的进行交互。

下面先建立一个新的数据库容器。

$ sudo docker run -d --name db training/postgres

删除以前建立的 web 容器

$ docker rm -f web

而后建立一个新的 web 容器,并将它链接到 db 容器

$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

此时,db 容器和 web 容器创建互联关系。

--link 参数的格式为 --link name:alias,其中 name 是要连接的容器的名称,alias 是这个链接的别名。

使用 docker ps 来查看容器的链接

$ docker ps
CONTAINER ID  IMAGE                     COMMAND               CREATED             STATUS             PORTS                    NAMES
349169744e49  training/postgres:latest  su postgres -c '/usr  About a minute ago  Up About a minute  5432/tcp                 db, web/db
aed84ee21bde  training/webapp:latest    python app.py         16 hours ago        Up 2 minutes       0.0.0.0:49154->5000/tcp  web

能够看到自定义命名的容器,db 和 web,db 容器的 names 列有 db 也有 web/db。这表示 web 容器连接到 db 容器,web 容器将被容许访问 db 容器的信息。

Docker 在两个互联的容器之间建立了一个安全隧道,并且不用映射它们的端口到宿主主机上。在启动 db 容器的时候并无使用 -p-P 标记,从而避免了暴露数据库端口到外部网络上。

Docker 经过 2 种方式为容器公开链接信息:

  • 环境变量
  • 更新 /etc/hosts 文件

使用 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 的文件。下面是父容器 web 的 hosts 文件

$ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7  aed84ee21bde
. . .
172.17.0.5  db

这里有 2 个 hosts,第一个是 web 容器,web 容器用 id 做为他的主机名,第二个是 db 容器的 ip 和主机名。
能够在 web 容器中安装 ping 命令来测试跟db容器的连通。

root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping
root@aed84ee21bde:/opt/webapp# ping db
PING db (172.17.0.5): 48 data bytes
56 bytes from 172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms
56 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms
56 bytes from 172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms

用 ping 来测试db容器,它会解析成 172.17.0.5
*注意:官方的 ubuntu 镜像默认没有安装 ping,须要自行安装。

用户能够连接多个父容器到子容器,好比能够连接多个 web 到 db 容器上。

资源连接

官方网站

实践参考

技术交流

其它

常见问题总结

这篇文章是我学习 Docker 的记录,大部份内容摘抄自 <<Docker — 从入门到实践>> 一书,并不是本人原创. 学习过程当中整理成适合我本身的笔记,其中也包含了我本身的实践记录.
相关文章
相关标签/搜索