这篇文章包含 docker 的基本概念,以及如何通过创建一个定制的 dockerfile 来 docker 化dockerize一个应用。
docker 是一个过去两年来从某个 idea 中孕育而生的有趣技术,公司组织们用它在世界上每个角落来部署应用。在今天的文章中,我将讲述如何通过“docker 化dockerize”一个现有的应用,来开始我们的 docker 之旅。这里提到的应用指的就是这个博客!
<a target="_blank"></a>
当我们开始学习 docker 基本概念时,让我们先去搞清楚什么是 docker 以及它为什么这么流行。docker 是一个操作系统容器管理工具,它通过将应用打包在操作系统容器中,来方便我们管理和部署应用。
容器和虚拟机并不完全相似,它是另外一种提供操作系统虚拟化的方式。它和标准的虚拟机还是有所不同。
标准的虚拟机一般会包括一个完整的操作系统、操作系统软件包、最后还有一至两个应用。这都得益于为虚拟机提供硬件虚拟化的管理程序。这样一来,一个单一的服务器就可以将许多独立的操作系统作为虚拟客户机运行了。
容器和虚拟机很相似,它们都支持在单一的服务器上运行多个操作环境,只是,在容器中,这些环境并不是一个个完整的操作系统。容器一般只包含必要的操作系统软件包和一些应用。它们通常不会包含一个完整的操作系统或者硬件的虚拟化。这也意味着容器比传统的虚拟机开销更少。
容器和虚拟机常被误认为是两种对立的技术。虚拟机采用一个物理服务器来提供全功能的操作环境,该环境会和其余虚拟机一起共享这些物理资源。容器一般用来隔离一个单一主机上运行的应用进程,以保证隔离后的进程之间不能相互影响。事实上,容器和 bsd jails 以及 <code>chroot</code> 进程的相似度,超过了和完整虚拟机的相似度。
现在,我们应该知道 docker 是什么了,然后,我们将从安装 docker,并部署一个公开的预构建好的容器开始,学习 docker 是如何工作的。
默认情况下,docker 并不会自动被安装在您的计算机中,所以,第一步就是安装 docker 软件包;我们的教学机器系统是 ubuntu 14.0.4,所以,我们将使用 apt 软件包管理器,来执行安装操作。
<code># apt-get install docker.io</code>
<code>reading package lists... done</code>
<code>building dependency tree</code>
<code>reading state information... done</code>
<code>the following extra packages will be installed:</code>
<code>aufs-tools cgroup-lite git git-man liberror-perl</code>
<code>suggested packages:</code>
<code>btrfs-tools debootstrap lxc rinse git-daemon-run git-daemon-sysvinit git-doc</code>
<code>git-el git-email git-gui gitk gitweb git-arch git-bzr git-cvs git-mediawiki</code>
<code>git-svn</code>
<code>the following new packages will be installed:</code>
<code>aufs-tools cgroup-lite docker.io git git-man liberror-perl</code>
<code>0 upgraded, 6 newly installed, 0 to remove and 0 not upgraded.</code>
<code>need to get 7,553 kb of archives.</code>
<code>after this operation, 46.6 mb of additional disk space will be used.</code>
<code>do you want to continue? [y/n] y</code>
为了检查当前是否有容器运行,我们可以执行<code>docker</code>命令,加上<code>ps</code>选项
<code># docker ps</code>
<code>container id image command created status ports names</code>
<code>docker</code>命令中的<code>ps</code>功能类似于 linux 的<code>ps</code>命令。它将显示可找到的 docker 容器及其状态。由于我们并没有启动任何 docker 容器,所以命令没有显示任何正在运行的容器。
我比较喜欢的 docker 特性之一就是 docker 部署预先构建好的容器的方式,就像<code>yum</code>和<code>apt-get</code>部署包一样。为了更好地解释,我们来部署一个运行着 nginx web 服务器的预构建容器。我们可以继续使用<code>docker</code>命令,这次选择<code>run</code>选项。
<code># docker run -d nginx</code>
<code>unable to find image 'nginx' locally</code>
<code>pulling repository nginx</code>
<code>5c82215b03d1: download complete</code>
<code>e2a4fb18da48: download complete</code>
<code>58016a5acc80: download complete</code>
<code>657abfa43d82: download complete</code>
<code>dcb2fe003d16: download complete</code>
<code>c79a417d7c6f: download complete</code>
<code>abb90243122c: download complete</code>
<code>d6137c9e2964: download complete</code>
<code>85e566ddc7ef: download complete</code>
<code>69f100eb42b5: download complete</code>
<code>cd720b803060: download complete</code>
<code>7cc81e9a118a: download complete</code>
<code>docker</code>命令的<code>run</code>选项,用来通知 docker 去寻找一个指定的 docker 镜像,然后启动运行着该镜像的容器。默认情况下,docker 容器运行在前台,这意味着当你运行<code>docker run</code>命令的时候,你的 shell 会被绑定到容器的控制台以及运行在容器中的进程。为了能在后台运行该 docker 容器,我们使用了<code>-d</code> (detach)标志。
再次运行<code>docker ps</code>命令,可以看到 nginx 容器正在运行。
<code>container id image command created status ports names</code>
<code>f6d31ab01fc9 nginx:latest nginx -g 'daemon off 4 seconds ago up 3 seconds 443/tcp, 80/tcp desperate_lalande</code>
从上面的输出信息中,我们可以看到正在运行的名为<code>desperate_lalande</code>的容器,它是由<code>nginx:latest image</code>(lctt 译注: nginx 最新版本的镜像)构建而来得。
镜像是 docker 的核心特征之一,类似于虚拟机镜像。和虚拟机镜像一样,docker 镜像是一个被保存并打包的容器。当然,docker 不只是创建镜像,它还可以通过 docker 仓库发布这些镜像,docker 仓库和软件包仓库的概念差不多,它让 docker 能够模仿<code>yum</code>部署软件包的方式来部署镜像。为了更好地理解这是怎么工作的,我们来回顾<code>docker run</code>执行后的输出。
我们可以看到第一条信息是,docker 不能在本地找到名叫 nginx 的镜像。这是因为当我们执行<code>docker run</code>命令时,告诉 docker 运行一个基于 nginx 镜像的容器。既然 docker 要启动一个基于特定镜像的容器,那么 docker 首先需要找到那个指定镜像。在检查远程仓库之前,docker 首先检查本地是否存在指定名称的本地镜像。
因为系统是崭新的,不存在 nginx 镜像,docker 将选择从 docker 仓库下载之。
和 github 一样,在 docker hub 创建公共仓库是免费的,私人仓库就需要缴纳费用了。当然,部署你自己的 docker 仓库也是可以的,事实上只需要简单地运行<code>docker run registry</code>命令就行了。但在这篇文章中,我们的重点将不是讲解如何部署一个定制的注册服务。
在我们继续构建定制容器之前,我们先清理一下 docker 环境,我们将关闭先前的容器,并移除它。
我们利用<code>docker</code>命令和<code>run</code>选项运行一个容器,所以,为了停止同一个容器,我们简单地在执行<code>docker</code>命令时,使用<code>kill</code>选项,并指定容器名。
<code># docker kill desperate_lalande</code>
<code>desperate_lalande</code>
当我们再次执行<code>docker ps</code>,就不再有容器运行了
但是,此时,我们这是停止了容器;虽然它不再运行,但仍然存在。默认情况下,<code>docker ps</code>只会显示正在运行的容器,如果我们附加<code>-a</code> (all) 标识,它会显示所有运行和未运行的容器。
<code># docker ps -a</code>
<code>container id image command created status ports names</code>
<code>f6d31ab01fc9 5c82215b03d1 nginx -g 'daemon off 4 weeks ago exited (-1) about a minute ago desperate_lalande</code>
为了能完整地移除容器,我们在用<code>docker</code>命令时,附加<code>rm</code>选项。
<code># docker rm desperate_lalande</code>
虽然容器被移除了;但是我们仍拥有可用的nginx镜像(lctt 译注:镜像缓存)。如果我们重新运行<code>docker run -d nginx</code>,docker 就无需再次拉取 nginx 镜像即可启动容器。这是因为我们本地系统中已经保存了一个副本。
为了列出系统中所有的本地镜像,我们运行<code>docker</code>命令,附加<code>images</code>选项。
<code># docker images</code>
<code>repository tag image id created virtual size</code>
<code>nginx latest 9fab4090484a 5 days ago 132.8 mb</code>
截至目前,我们已经使用了一些基础的 docker 命令来启动、停止和移除一个预构建好的普通镜像。为了“docker 化(dockerize)”这篇博客,我们需要构建我们自己的镜像,也就是创建一个 dockerfile。
在大多数虚拟机环境中,如果你想创建一个机器镜像,首先,你需要建立一个新的虚拟机、安装操作系统、安装应用,最后将其转换为一个模板或者镜像。但在 docker 中,所有这些步骤都可以通过 dockerfile 实现全自动。dockerfile 是向 docker 提供构建指令去构建定制镜像的方式。在这一章节,我们将编写能用来部署这个博客的定制 dockerfile。
我们开始构建 dockerfile 之前,第一步要搞明白,我们需要哪些东西来部署这个博客。
<code># git clone https://github.com/madflojo/blog.git</code>
<code>cloning into 'blog'...</code>
<code>remote: counting objects: 622, done.</code>
<code>remote: total 622 (delta 0), reused 0 (delta 0), pack-reused 622</code>
<code>receiving objects: 100% (622/622), 14.80 mib | 1.06 mib/s, done.</code>
<code>resolving deltas: 100% (242/242), done.</code>
<code>checking connectivity... done.</code>
<code># cd blog/</code>
<code># vi dockerfile</code>
第一条 dockerfile 指令是<code>from</code>指令。这将指定一个现存的镜像作为我们的基础镜像。这也从根本上给我们提供了继承其他 docker 镜像的途径。在本例中,我们还是从刚刚我们使用的 nginx 开始,如果我们想从头开始,我们可以通过指定<code>ubuntu:latest</code>来使用 ubuntu docker 镜像。
<code>## dockerfile that generates an instance of http://bencane.com</code>
<code></code>
<code>from nginx:latest</code>
<code>maintainer benjamin cane <[email protected]></code>
除了<code>from</code>指令,我还使用了<code>maintainer</code>,它用来显示 dockerfile 的作者。
docker 支持使用<code>#</code>作为注释,我将经常使用该语法,来解释 dockerfile 的部分内容。
想要从 dockerfile 构建镜像,我们只需要在运行 <code>docker</code> 命令的时候,加上 <code>build</code> 选项。
<code># docker build -t blog /root/blog</code>
<code>sending build context to docker daemon 23.6 mb</code>
<code>sending build context to docker daemon</code>
<code>step 0 : from nginx:latest</code>
<code>---> 9fab4090484a</code>
<code>step 1 : maintainer benjamin cane <[email protected]></code>
<code>---> running in c97f36450343</code>
<code>---> 60a44f78d194</code>
<code>removing intermediate container c97f36450343</code>
<code>successfully built 60a44f78d194</code>
上面的例子,我们使用了<code>-t</code> (tag)标识给镜像添加“blog”的标签。实质上我们就是在给镜像命名,如果我们不指定标签,就只能通过 docker 分配的 image id 来访问镜像了。本例中,从 docker 构建成功的信息可以看出,image id值为 <code>60a44f78d194</code>。
除了<code>-t</code>标识外,我还指定了目录<code>/root/blog</code>。该目录被称作“构建目录”,它将包含 dockerfile,以及其它需要构建该容器的文件。
现在我们构建成功了,下面我们开始定制该镜像。
用来生成 html 页面的静态站点生成器是用 python 语言编写的,所以,在 dockerfile 中需要做的第一件定制任务是安装 python。我们将使用 apt 软件包管理器来安装 python 软件包,这意味着在 dockerfile 中我们要指定运行<code>apt-get update</code>和<code>apt-get install python-dev</code>;为了完成这一点,我们可以使用<code>run</code>指令。
<code>## install python and pip</code>
<code>run apt-get update</code>
<code>run apt-get install -y python-dev python-pip</code>
如上所示,我们只是简单地告知 docker 构建镜像的时候,要去执行指定的<code>apt-get</code>命令。比较有趣的是,这些命令只会在该容器的上下文中执行。这意味着,即使在容器中安装了<code>python-dev</code>和<code>python-pip</code>,但主机本身并没有安装这些。说的更简单点,<code>pip</code>命令将只在容器中执行,出了容器,<code>pip</code>命令不存在。
还有一点比较重要的是,docker 构建过程中不接受用户输入。这说明任何被<code>run</code>指令执行的命令必须在没有用户输入的时候完成。由于很多应用在安装的过程中需要用户的输入信息,所以这增加了一点难度。不过我们例子中,<code>run</code>命令执行的命令都不需要用户输入。
python 安装完毕后,我们现在需要安装 python 模块。如果在 docker 外做这些事,我们通常使用<code>pip</code>命令,然后参考我的博客 git 仓库中名叫<code>requirements.txt</code>的文件。在之前的步骤中,我们已经使用<code>git</code>命令成功地将 github 仓库“克隆”到了<code>/root/blog</code>目录;这个目录碰巧也是我们创建<code>dockerfile</code>的目录。这很重要,因为这意味着 docker 在构建过程中可以访问这个 git 仓库中的内容。
当我们执行构建后,docker 将构建的上下文环境设置为指定的“构建目录”。这意味着目录中的所有文件都可以在构建过程中被使用,目录之外的文件(构建环境之外)是不能访问的。
为了能安装所需的 python 模块,我们需要将<code>requirements.txt</code>从构建目录拷贝到容器中。我们可以在<code>dockerfile</code>中使用<code>copy</code>指令完成这一需求。
<code>## create a directory for required files</code>
<code>run mkdir -p /build/</code>
<code>## add requirements file and run pip</code>
<code>copy requirements.txt /build/</code>
<code>run pip install -r /build/requirements.txt</code>
在<code>dockerfile</code>中,我们增加了3条指令。第一条指令使用<code>run</code>在容器中创建了<code>/build/</code>目录。该目录用来拷贝生成静态 html 页面所需的一切应用文件。第二条指令是<code>copy</code>指令,它将<code>requirements.txt</code>从“构建目录”(<code>/root/blog</code>)拷贝到容器中的<code>/build/</code>目录。第三条使用<code>run</code>指令来执行<code>pip</code>命令;安装<code>requirements.txt</code>文件中指定的所有模块。
当构建定制镜像时,<code>copy</code>是条重要的指令。如果在 dockerfile 中不指定拷贝文件,docker 镜像将不会包含requirements.txt 这个文件。在 docker 容器中,所有东西都是隔离的,除非在 dockerfile 中指定执行,否则容器中不会包括所需的依赖。
现在,我们让 docker 执行了一些定制任务,现在我们尝试另一次 blog 镜像的构建。
<code>sending build context to docker daemon 19.52 mb</code>
<code>---> using cache</code>
<code>---> 8e0f1899d1eb</code>
<code>step 2 : run apt-get update</code>
<code>---> 78b36ef1a1a2</code>
<code>step 3 : run apt-get install -y python-dev python-pip</code>
<code>---> ef4f9382658a</code>
<code>step 4 : run mkdir -p /build/</code>
<code>---> running in bde05cf1e8fe</code>
<code>---> f4b66e09fa61</code>
<code>removing intermediate container bde05cf1e8fe</code>
<code>step 5 : copy requirements.txt /build/</code>
<code>---> cef11c3fb97c</code>
<code>removing intermediate container 9aa8ff43f4b0</code>
<code>step 6 : run pip install -r /build/requirements.txt</code>
<code>---> running in c50b15ddd8b1</code>
<code>downloading/unpacking jinja2 (from -r /build/requirements.txt (line 1))</code>
<code>downloading/unpacking pyyaml (from -r /build/requirements.txt (line 2))</code>
<code><truncated to reduce noise></code>
<code>successfully installed jinja2 pyyaml mistune markdown markupsafe</code>
<code>cleaning up...</code>
<code>---> abab55c20962</code>
<code>removing intermediate container c50b15ddd8b1</code>
<code>successfully built abab55c20962</code>
上述输出所示,我们可以看到构建成功了,我们还可以看到另外一个有趣的信息<code>---> using cache</code>。这条信息告诉我们,docker 在构建该镜像时使用了它的构建缓存。
当 docker 构建镜像时,它不仅仅构建一个单独的镜像;事实上,在构建过程中,它会构建许多镜像。从上面的输出信息可以看出,在每一“步”执行后,docker 都在创建新的镜像。
上面片段的最后一行可以看出,docker 在告诉我们它在创建一个新镜像,因为它打印了image id :<code>cef11c3fb97c</code>。这种方式有用之处在于,docker能在随后构建这个 blog 镜像时将这些镜像作为缓存使用。这很有用处,因为这样, docker 就能加速同一个容器中新构建任务的构建流程。从上面的例子中,我们可以看出,docker 没有重新安装<code>python-dev</code>和<code>python-pip</code>包,docker 则使用了缓存镜像。但是由于 docker 并没有找到执行<code>mkdir</code>命令的构建缓存,随后的步骤就被一一执行了。
docker 构建缓存一定程度上是福音,但有时也是噩梦。这是因为决定使用缓存或者重新运行指令的因素很少。比如,如果<code>requirements.txt</code>文件发生了修改,docker 会在构建时检测到该变化,然后 docker 会重新执行该执行那个点往后的所有指令。这得益于 docker 能查看<code>requirements.txt</code>的文件内容。但是,<code>apt-get</code>命令的执行就是另一回事了。如果提供 python 软件包的 apt 仓库包含了一个更新的 python-pip 包;docker 不会检测到这个变化,转而去使用构建缓存。这会导致之前旧版本的包将被安装。虽然对<code>python-pip</code>来说,这不是主要的问题,但对使用了存在某个致命攻击缺陷的软件包缓存来说,这是个大问题。
出于这个原因,抛弃 docker 缓存,定期地重新构建镜像是有好处的。这时,当我们执行 docker 构建时,我简单地指定<code>--no-cache=true</code>即可。
python 软件包和模块安装后,接下来我们将拷贝需要用到的应用文件,然后运行<code>hamerkop</code>应用。我们只需要使用更多的<code>copy</code> 和 <code>run</code>指令就可完成。
<code>## add blog code nd required files</code>
<code>copy static /build/static</code>
<code>copy templates /build/templates</code>
<code>copy hamerkop /build/</code>
<code>copy config.yml /build/</code>
<code>copy articles /build/articles</code>
<code>## run generator</code>
<code>run /build/hamerkop -c /build/config.yml</code>
现在我们已经写出了剩余的构建指令,我们再次运行另一次构建,并确保镜像构建成功。
<code># docker build -t blog /root/blog/</code>
<code>step 7 : copy static /build/static</code>
<code>---> 15cb91531038</code>
<code>removing intermediate container d478b42b7906</code>
<code>step 8 : copy templates /build/templates</code>
<code>---> ecded5d1a52e</code>
<code>removing intermediate container ac2390607e9f</code>
<code>step 9 : copy hamerkop /build/</code>
<code>---> 59efd1ca1771</code>
<code>removing intermediate container b5fbf7e817b7</code>
<code>step 10 : copy config.yml /build/</code>
<code>---> bfa3db6c05b7</code>
<code>removing intermediate container 1aebef300933</code>
<code>step 11 : copy articles /build/articles</code>
<code>---> 6b61cc9dde27</code>
<code>removing intermediate container be78d0eb1213</code>
<code>step 12 : run /build/hamerkop -c /build/config.yml</code>
<code>---> running in fbc0b5e574c5</code>
<code>successfully created file /usr/share/nginx/html//2011/06/25/checking-the-number-of-lwp-threads-in-linux</code>
<code>successfully created file /usr/share/nginx/html//2011/06/checking-the-number-of-lwp-threads-in-linux</code>
<code>successfully created file /usr/share/nginx/html//archive.html</code>
<code>successfully created file /usr/share/nginx/html//sitemap.xml</code>
<code>---> 3b25263113e1</code>
<code>removing intermediate container fbc0b5e574c5</code>
<code>successfully built 3b25263113e1</code>
成功的一次构建后,我们现在就可以通过运行<code>docker</code>命令和<code>run</code>选项来运行我们定制的容器,和之前我们启动 nginx 容器一样。
<code># docker run -d -p 80:80 --name=blog blog</code>
<code>5f6c7a2217dcdc0da8af05225c4d1294e3e6bb28a41ea898a1c63fb821989ba1</code>
我们这次又使用了<code>-d</code> (detach)标识来让docker在后台运行。但是,我们也可以看到两个新标识。第一个新标识是<code>--name</code>,这用来给容器指定一个用户名称。之前的例子,我们没有指定名称,因为 docker 随机帮我们生成了一个。第二个新标识是<code>-p</code>,这个标识允许用户从主机映射一个端口到容器中的一个端口。
之前我们使用的基础 nginx 镜像分配了80端口给 http 服务。默认情况下,容器内的端口通道并没有绑定到主机系统。为了让外部系统能访问容器内部端口,我们必须使用<code>-p</code>标识将主机端口映射到容器内部端口。上面的命令,我们通过<code>-p 80:80</code>语法将主机80端口映射到容器内部的80端口。
经过上面的命令,我们的容器看起来成功启动了,我们可以通过执行<code>docker ps</code>核实。
<code>container id image command created status ports names</code>
<code>d264c7ef92bd blog:latest nginx -g 'daemon off 3 seconds ago up 3 seconds 443/tcp, 0.0.0.0:80->80/tcp blog</code>
原文发布时间为:2016-06-08
本文来自云栖社区合作伙伴“linux中国”