这篇文章是我学习 Docker 的记录,大部份内容摘抄自 <<Docker — 从入门到实践>> 一书,并不是本人原创.
学习过程当中整理成适合我本身的笔记,其中也包含了我本身的实践记录.
最近工做中遇到项目部署的问题,由于原先旧项目还须要继续在线服役,因此生产环境的一整套东西一直都停留在很低版本的 CentOS 中,不少时候想扩展或想部署一个新功能由于生产环境的问题而不得不花费更多的时间,有时候还不得不放弃.最要命的是咱们新项目的开发环境是 Windows 环境,并且都是用较新的开发环境;而测试环境却又是较新的 CentOS 环境,致使不少时候在这个环境运行没有问题,在另外一个环境却平白无故出问题,期间为了这些事浪费了不少时间.还好发现有 Docker 可以解决这些头痛的问题,固然 Docker 不仅仅只能解决以上问题,它还有不少强大的功能.接下来就从零开始讲讲 Docker.php
Docker 是 Docker 公司的开源项目,使用 Google 公司推出的 Go 语言开发的,并于 2013 年 3 月以 Apache 2.0 受权协议开源,主要项目代码在 GitHub 上进行维护。html
下面的图片比较了 Docker 和传统虚拟化方式的不一样之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操做系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有本身的内核,并且也没有进行硬件虚拟。所以容器要比传统虚拟机更为轻便。node
Docker 跟传统的虚拟化方式相比具备如下优点:python
因为容器不须要进行硬件虚拟以及运行完整操做系统等额外开销,Docker 对系统资源的利用率更高。不管是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。所以,相比虚拟机技术,一个相同配置的主机,每每能够运行更多数量的应用。mysql
传统的虚拟机技术启动应用服务每每须要数分钟,而 Docker 容器应用,因为直接运行于宿主内核,无需启动完整的操做系统,所以能够作到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。linux
开发过程当中一个常见的问题是环境一致性问题。因为开发环境、测试环境、生产环境不一致,致使有些 bug 并未在开发过程当中被发现。而 Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 “这段代码在我机器上没问题啊” 这类问题。nginx
对开发和运维人员来讲,最但愿的就是一次建立或配置,能够在任意地方正常运行。git
使用 Docker 能够经过定制应用镜像来实现持续集成、持续交付、部署。开发人员能够经过 Dockerfile 来进行镜像构建,并结合 持续集成系统进行集成测试,而运维人员则能够直接在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署。github
并且使用 Dockerfile 使镜像构建透明化,不只仅开发团队能够理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像。web
因为 Docker 确保了执行环境的一致性,使得应用的迁移更加容易。Docker 能够在不少平台上运行,不管是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的。所以用户能够很轻易的将在一个平台上运行的应用,迁移到另外一个平台上,而不用担忧运行环境的变化致使应用没法正常运行的状况。
Docker 使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得很是简单。此外,Docker 团队同各个开源项目团队一块儿维护了一大批高质量的官方镜像,既能够直接在生产环境使用,又能够做为基础进一步定制,大大的下降了应用服务的镜像制做成本。
Docker 包括三个基本概念
理解了这三个概念,就理解了 Docker 的整个生命周期。
咱们都知道,操做系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root
文件系统为其提供用户空间支持。而 Docker 镜像,就至关因而一个 root
文件系统。好比 Docker 官方镜像 ubuntu:14.04
就包含了完整的一套 Ubuntu 14.04 最小系统的 root
文件系统。
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建以后也不会被改变。
镜像和容器的关系,就像是面向对象程序设计中的类
和实例
同样,镜像是静态的定义,容器是镜像运行时的实体。容器能够被建立、启动、中止、删除、暂停等。
每个容器运行时,是以镜像为基础层,在其上建立一个当前容器的存储层,咱们能够称这个为容器运行时读写而准备的存储层为容器存储层。
容器存储层的生存周期和容器同样,容器消亡时,容器存储层也随之消亡。所以,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不该该向其存储层内写入任何数据,容器存储层要保持无状态化。全部的文件写入操做,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。所以,使用数据卷后,容器能够随意删除、从新 run
,数据却不会丢失。
镜像构建完成后,能够很容易的在当前宿主上运行,可是,若是须要在其它服务器上使用这个镜像,咱们就须要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
官方网站上有各类环境下的 安装指南,这里主要介绍下 CentOS 的安装。
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 -v > Docker version 17.05.0-ce, build 89658be
能够看出当前的 Docker 为 Docker CE 17.05.0 版本,CE 表明 Docker 社区版,EE 表明 Docker 企业版.
卸载Docker软件包:
$ yum remove docker-ce
较老版本的 Docker 被称为 docker 或 docker-engine。若是这些已安装,请卸载它们以及关联的依赖关系。
$ yum remove docker docker-common docker-selinux docker-engine
主机上的图像,容器,卷或自定义配置文件不会自动删除。必须手动删除任何已编辑的配置文件。删除全部图像,容器和卷:
$ rm -rf /var/lib/docker
国内访问 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 Hub 公共注册服务器中的仓库)。
阿里云镜像库 上有大量的高质量的镜像能够用,这里咱们就说一下怎么获取这些镜像并运行。
获取镜像的命令是 docker pull
。其命令格式为:
docker pull [选项] [Docker Registry地址]<仓库名>:<标签>
具体的选项能够经过 docker pull --help
命令看到,这里咱们说一下镜像名称的格式。
<域名/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 欢迎页面。
如今,改动这个欢迎页面,改为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
的内容。
如今咱们再刷新浏览器的话,会发现内容被改变了。
咱们修改了容器的文件,也就是改动了容器的存储层。咱们能够经过 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 是一个文本文件,其内包含了一条条的指令(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 很简单,一共就两行。涉及到了两条指令,FROM
和 RUN
。
所谓定制镜像,那必定是以一个镜像为基础,在其上进行定制。就像咱们以前运行了一个 nginx
镜像的容器,再进行修改同样,基础镜像是必须指定的。而 FROM
就是指定基础镜像,所以一个 Dockerfile
中 FROM
是必备的指令,而且必须是第一条指令。
RUN
指令是用来执行命令行命令的。因为命令行的强大能力,RUN
指令在定制镜像时是最经常使用的指令之一。其格式有两种:
RUN <命令>
,就像直接在命令行中输入的命令同样。刚才写的 Dockrfile 中的 RUN
指令就是这种格式。RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
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
同样。
若是注意,会看到 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
,以及会将其置于镜像构建上下文目录中。
格式:
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
和 RUN
指令同样,也有两种格式,一种相似于命令行,一种相似于函数调用。
COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。好比:
COPY package.json /usr/src/app/
<源路径>
能够是多个,甚至能够是通配符,如:
COPY hom* /mydir/ COPY hom?.txt /mydir/
<目标路径>
能够是容器内的绝对路径,也能够是相对于工做目录的相对路径(工做目录能够用 WORKDIR
指令来指定)。目标路径不须要事先建立,若是目录不存在会在复制文件前先行建立缺失目录。
此外,还须要注意一点,使用 COPY
指令,源文件的各类元数据都会保留。好比读、写、执行权限、文件变动时间等。这个特性对于镜像定制颇有用。特别是构建相关文件都在使用 Git 进行管理的时候。
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
指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
所以在 COPY
和 ADD
指令中选择的时候,能够遵循这样的原则,全部的文件复制均使用 COPY
指令,仅在须要自动解压缩的场合使用 ADD
。
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
的格式和 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 用户,并在最后指定了 ENTRYPOINT
为 docker-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 <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
构建维护变得更轻松了。
下列指令能够支持环境变量引用: ADD
、COPY
、ENV
、EXPOSE
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
。
能够从这个指令列表里感受到,环境变量可使用的地方不少,很强大。经过环境变量,咱们可让一份 Dockerfile
制做更多的镜像,只需使用不一样的环境变量便可。
格式: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 ["<路径1>", "<路径2>"...]
VOLUME <路径>
以前说过,容器运行时应该尽可能保持容器存储层不发生写操做,对于数据库类须要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节咱们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile
中,咱们能够事先指定某些目录挂载为匿名卷,这样在运行时若是用户不指定挂载,其应用也能够正常运行,不会向容器存储层写入大量数据。
VOLUME /data
这里的 /data
目录就会在运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。固然,运行时能够覆盖这个挂载设置。好比:
docker run -d -v mydata:/data xxxx
在这行命令中,就使用了 mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷的挂载配置。
格式为 EXPOSE <端口1> [<端口2>...]
。
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会由于这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另外一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
格式为 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 [选项] 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
来帮助判断,其 Dockerfile
的 HEALTHCHECK
能够这么写:
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
是一个特殊的指令,它后面跟的是其它指令,好比 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
、镜像名
或者 镜像摘要
。
好比咱们有这么一些镜像:
$ 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 -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 又一核心概念。
简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机能够理解为模拟运行的一整套操做系统(提供了运行态环境和其余系统环境)和跑在上面的应用。
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另一个是将在终止状态(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
容器的核心为所执行的应用程序,所须要的资源都是应用程序运行所必需的。除此以外,并无其它的资源。能够在伪终端中利用 ps
或 top
来查看进程信息。
root@ba267838cc1b:/# ps PID TTY TIME CMD 1 ? 00:00:00 bash 11 ? 00:00:00 ps
可见,容器中仅运行了指定的 bash 应用。这种特色使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。
当利用 docker run
来建立容器时,Docker 在后台运行的标准操做包括:
利用 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 ID
或 NAMES
,直接将一个已经终止的容器启动运行。
$ 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 ID
或 NAMES
,来终止一个运行中的容器。
$ 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
工具等。
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
工具在 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 镜像来运行。
$ 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
对于 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 tag
将 ba58
这个镜像标记为 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
数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 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 容许经过外部访问容器或容器互联的方式来提供网络服务。
容器中能够运行一些网络应用,要让外部也能够访问这些应用,能够经过 -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.
注意:
docker inspect
能够获取全部的变量,Docker 还能够有一个可变的网络配置。)例如
$ sudo docker run -d -p 5000:5000 -p 3000:80 training/webapp python app.py
容器的链接(linking)系统是除了端口映射外,另外一种跟容器中应用交互的方式。
该系统会在源和接收容器之间建立一个隧道,接收容器能够看到源容器指定的信息。
链接系统依据容器的名称来执行。所以,首先须要自定义一个好记的容器命名。
虽然当建立容器的时候,系统默认会分配一个名字。自定义命名容器有2个好处:
使用 --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 — 从入门到实践>> 一书,并不是本人原创. 学习过程当中整理成适合我本身的笔记,其中也包含了我本身的实践记录.