26 KiB
通过Dockerize这篇博客来开启我们的Docker之旅
这篇文章将包含Docker的基本概念,以及如何通过创建一个定制的Dockerfile来Dockerize一个应用 作者:Benjamin Cane,2015-12-01 10:00:00
Docker是2年前从某个idea中孕育而生的有趣技术,世界各地的公司组织都积极使用它来部署应用。在今天的文章中,我将教你如何通过"Dockerize"一个现有的应用,来开始我们的Docker运用。问题中的应用指的就是这篇博客!
什么是Docker?
当我们开始学习Docker基本概念时,让我们先去搞清楚什么是Docker以及它为什么这么流行。Docker是一个操作系统容器管理工具,它通过将应用打包在操作系统容器中,来方便我们管理和部署应用。
容器 vs. 虚拟机
容器虽和虚拟机并不完全相似,但它也是一种提供操作系统虚拟化的方式。但是,它和标准的虚拟机还是有不同之处的。
标准虚拟机一般会包括一个完整的操作系统,操作系统包,最后还有一至两个应用。这都得益于为虚拟机提供硬件虚拟化的管理程序。这样一来,一个单一的服务器就可以将许多独立的操作系统作为虚拟客户机运行了。
容器和虚拟机很相似,它们都支持在单一的服务器上运行多个操作环境,只是,在容器中,这些环境并不是一个个完整的操作系统。容器一般只包含必要的操作系统包和一些应用。它们通常不会包含一个完整的操作系统或者硬件虚拟化程序。这也意味着容器比传统的虚拟机开销更少。
容器和虚拟机常被误认为是两种抵触的技术。虚拟机采用同一个物理服务器,来提供全功能的操作环境,该环境会和其余虚拟机一起共享这些物理资源。容器一般用来隔离运行中的应用进程,运行进程将在单独的主机中运行,以保证隔离后的进程之间不能相互影响。事实上,容器和BSD Jails以及chroot
进程的相似度,超过了和完整虚拟机的相似度。
Docker在容器的上层提供了什么
Docker不是一个容器运行环境,事实上,只是一个容器技术,并不包含那些帮助Docker支持Solaris Zones和BSD Jails的技术。Docker提供管理,打包和部署容器的方式。虽然一定程度上,虚拟机多多少少拥有这些类似的功能,但虚拟机并没有完整拥有绝大多数的容器功能,即使拥有,这些功能用起来都并没有Docker来的方便。
现在,我们应该知道Docker是什么了,然后,我们将从安装Docker,并部署一个公共的预构建好的容器开始,学习Docker是如何工作的。
从安装开始
默认情况下,Docker并不会自动被安装在您的计算机中,所以,第一步就是安装Docker包;我们的教学机器系统是Ubuntu 14.0.4,所以,我们将使用Apt包管理器,来执行安装操作。
# apt-get install docker.io
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
aufs-tools cgroup-lite git git-man liberror-perl
Suggested packages:
btrfs-tools debootstrap lxc rinse git-daemon-run git-daemon-sysvinit git-doc
git-el git-email git-gui gitk gitweb git-arch git-bzr git-cvs git-mediawiki
git-svn
The following NEW packages will be installed:
aufs-tools cgroup-lite docker.io git git-man liberror-perl
0 upgraded, 6 newly installed, 0 to remove and 0 not upgraded.
Need to get 7,553 kB of archives.
After this operation, 46.6 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
为了检查当前是否有容器运行,我们可以执行docker
命令,加上ps
选项
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
docker
命令中的ps
功能类似于Linux的ps
命令。它将显示可找到的Docker容器以及各自的状态。由于我们并没有开启任何Docker容器,所以命令没有显示任何正在运行的容器。
部署一个预构建好的nginx Docker容器
我比较喜欢的Docker特性之一就是Docker部署预先构建好的容器的方式,就像yum
和apt-get
部署包一样。为了更好地解释,我们来部署一个运行着nginx web服务器的预构建容器。我们可以继续使用docker
命令,这次选择run
选项。
# docker run -d nginx
Unable to find image 'nginx' locally
Pulling repository nginx
5c82215b03d1: Download complete
e2a4fb18da48: Download complete
58016a5acc80: Download complete
657abfa43d82: Download complete
dcb2fe003d16: Download complete
c79a417d7c6f: Download complete
abb90243122c: Download complete
d6137c9e2964: Download complete
85e566ddc7ef: Download complete
69f100eb42b5: Download complete
cd720b803060: Download complete
7cc81e9a118a: Download complete
docker
命令的run
选项,用来通知Docker去寻找一个指定的Docker镜像,然后开启运行着该镜像的容器。默认情况下,Docker容器在前台运行,这意味着当你运行docker run
命令的时候,你的shell会被绑定到容器的控制台以及运行在容器中的进程。为了能在后台运行该Docker容器,我们可以使用-d
(detach)标志。
再次运行docker ps
命令,可以看到nginx容器正在运行。
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6d31ab01fc9 nginx:latest nginx -g 'daemon off 4 seconds ago Up 3 seconds 443/tcp, 80/tcp desperate_lalande
从上面的打印信息中,我们可以看到正在运行的名为desperate_lalande
的容器,它是由nginx:latest image
(译者注:nginx最新版本的镜像)构建而来得。
Docker镜像
镜像是Docker的核心特征之一,类似于虚拟机镜像。和虚拟机镜像一样,Docker镜像是一个被保存并打包的容器。当然,Docker不只是创建镜像,它还可以通过Docker仓库发布这些镜像,Docker仓库和包仓库的概念差不多,它让Docker能够模仿yum
部署包的方式来部署镜像。为了更好地理解这是怎么工作的,我们来回顾docker run
执行后的输出。
# docker run -d nginx
Unable to find image 'nginx' locally
我们可以看到第一条信息是,Docker不能在本地找到名叫nginx的镜像。这是因为当我们执行docker run
命令时,告诉Docker运行一个基于nginx镜像的容器。既然Docker要启动一个基于特定镜像的容器,那么Docker首先需要知道那个指定镜像。在检查远程仓库之前,Docker首先检查本地是否存在指定名称的本地镜像。
因为系统是崭新的,不存在nginx镜像,Docker将选择从Docker仓库下载之。
Pulling repository nginx
5c82215b03d1: Download complete
e2a4fb18da48: Download complete
58016a5acc80: Download complete
657abfa43d82: Download complete
dcb2fe003d16: Download complete
c79a417d7c6f: Download complete
abb90243122c: Download complete
d6137c9e2964: Download complete
85e566ddc7ef: Download complete
69f100eb42b5: Download complete
cd720b803060: Download complete
7cc81e9a118a: Download complete
这就是第二部分打印信息显示给我们的内容。默认,Docker会使用Docker Hub仓库,该仓库由Docker公司维护。
和Github一样,在Docker Hub创建公共仓库是免费的,私人仓库就需要缴纳费用了。当然,部署你自己的Docker仓库也是可以实现的,事实上只需要简单地运行docker run registry
命令就行了。但在这篇文章中,我们的重点将不是讲解如何部署一个定制的注册服务。
关闭并移除容器
在我们继续构建定制容器之前,我们先清理Docker环境,我们将关闭先前的容器,并移除它。
我们利用docker
命令和run
选项运行一个容器,所以,为了停止该相同的容器,我们简单地在执行docker
命令时,使用kill
选项,并指定容器名。
# docker kill desperate_lalande
desperate_lalande
当我们再次执行docker ps
,就不再有容器运行了
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
但是,此时,我们这是停止了容器;虽然它不再运行,但仍然存在。默认情况下,docker ps
只会显示正在运行的容器,如果我们附加-a
(all) 标识,它会显示所有运行和未运行的容器。
# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f6d31ab01fc9 5c82215b03d1 nginx -g 'daemon off 4 weeks ago Exited (-1) About a minute ago desperate_lalande
为了能完整地移除容器,我们在用docker
命令时,附加rm
选项。
# docker rm desperate_lalande
desperate_lalande
虽然容器被移除了;但是我们仍拥有可用的nginx镜像(译者注:镜像缓存)。如果我们重新运行docker run -d nginx
,Docker就无需再次拉取nginx镜像,即可启动容器。这是因为我们本地系统中已经保存了一个副本。
为了列出系统中所有的本地镜像,我们运行docker
命令,附加images
选项。
# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
nginx latest 9fab4090484a 5 days ago 132.8 MB
构建我们自己的镜像
截至目前,我们已经使用了一些基础的Docker命令来开启,停止和移除一个预构建好的普通镜像。为了"Dockerize"这篇博客,我们需要构建我们自己的镜像,也就是创建一个Dockerfile。
在大多数虚拟机环境中,如果你想创建一个机器镜像,首先,你需要建立一个新的虚拟机,安装操作系统,安装应用,最后将其转换为一个模板或者镜像。但在Docker中,所有这些步骤都可以通过Dockerfile实现全自动。Dockerfile是向Docker提供构建指令去构建定制镜像的方式。在这一章节,我们将编写能用来部署这篇博客的定制Dockerfile。
理解应用
我们开始构建Dockerfile之前,第一步要搞明白,我们需要哪些东西来部署这篇博客。
博客本质上是由静态站点生成器生成的静态HTML页面,这个静态站点是我编写的,名为hamerkop。这个生成器很简单,它所做的就是生成该博客站点。所有的博客源码都被我放在了一个公共的Github仓库。为了部署这篇博客,我们要先从Github仓库把博客内容拉取下来,然后安装Python和一些Python模块,最后执行hamerkop
应用。我们还需要安装nginx,来运行生成后的内容。
截止目前,这些还是一个简单的Dockerfile,但它却给我们展示了相当多的Dockerfile语法。我们需要克隆Github仓库,然后使用你最喜欢的编辑器编写Dockerfile;我选择vi
# git clone https://github.com/madflojo/blog.git
Cloning into 'blog'...
remote: Counting objects: 622, done.
remote: Total 622 (delta 0), reused 0 (delta 0), pack-reused 622
Receiving objects: 100% (622/622), 14.80 MiB | 1.06 MiB/s, done.
Resolving deltas: 100% (242/242), done.
Checking connectivity... done.
# cd blog/
# vi Dockerfile
FROM - 继承一个Docker镜像
第一条Dockerfile指令是FROM
指令。这将指定一个现存的镜像作为我们的基础镜像。这也从根本上给我们提供了继承其他Docker镜像的途径。在本例中,我们还是从刚刚我们使用的nginx开始,如果我们想重新开始,我们可以通过指定ubuntu:latest
来使用Ubuntu Docker镜像。
## Dockerfile that generates an instance of http://bencane.com
FROM nginx:latest
MAINTAINER Benjamin Cane <ben@bencane.com>
除了FROM
指令,我还使用了MAINTAINER
,它用来显示Dockerfile的作者。
Docker支持使用#
作为注释,我将经常使用该语法,来解释Dockerfile的部分内容。
运行一次测试构建
因为我们继承了nginx Docker镜像,我们现在的Dockerfile也就包括了用来构建nginx镜像的Dockerfile中所有指令。这意味着,此时我们可以从该Dockerfile中构建出一个Docker镜像,然后从该镜像中运行一个容器。虽然,最终的镜像和nginx镜像本质上是一样的,但是我们这次是通过构建Dockerfile的形式,然后我们将讲解Docker构建镜像的过程。
想要从Dockerfile构建镜像,我们只需要在运行docker
命令的时候,加上build选项。
# docker build -t blog /root/blog
Sending build context to Docker daemon 23.6 MB
Sending build context to Docker daemon
Step 0 : FROM nginx:latest
---> 9fab4090484a
Step 1 : MAINTAINER Benjamin Cane <ben@bencane.com>
---> Running in c97f36450343
---> 60a44f78d194
Removing intermediate container c97f36450343
Successfully built 60a44f78d194
上面的例子,我们使用了-t
(tag)标识给镜像添加"blog"的标签。本质上我们只是在给镜像命名,如果我们不指定标签,就只能通过Docker分配的Image ID来访问镜像了。本例中,从Docker构建成功的信息可以看出,Image ID值为60a44f78d194
。
除了-t
标识外,我还指定了目录/root/blog
。该目录被称作"构建目录",它将包含Dockerfile,以及其他需要构建该容器的文件。
现在我们构建成功,下面我们开始定制该镜像。
使用RUN来执行apt-get
用来生成HTML页面的静态站点生成器是用Python语言编写的,所以,在Dockerfile中需要做的第一件定制任务是安装Python。我们将使用Apt包管理器来安装Python包,这意味着在Dockerfile中我们要指定运行apt-get update
和apt-get install python-dev
;为了完成这一点,我们可以使用RUN
指令。
## Dockerfile that generates an instance of http://bencane.com
FROM nginx:latest
MAINTAINER Benjamin Cane <ben@bencane.com>
## Install python and pip
RUN apt-get update
RUN apt-get install -y python-dev python-pip
如上所示,我们只是简单地告知Docker构建镜像的时候,要去执行指定的apt-get
命令。比较有趣的是,这些命令只会在该容器的上下文中执行。这意味着,即使容器中安装了python-dev
和python-pip
,但主机本身并没有安装这些。说的更简单点,pip
命令将只在容器中执行,出了容器,pip
命令不存在。
还有一点比较重要的是,Docker构建过程中不接受用户输入。这说明任何被RUN
指令执行的命令必须在没有用户输入的时候完成。由于很多应用在安装的过程中需要用户的输入信息,所以这增加了一点难度。我们例子,RUN
命令执行的命令都不需要用户输入。
安装Python模块
Python安装完毕后,我们现在需要安装Python模块。如果在Docker外做这些事,我们通常使用pip
命令,然后参考博客Git仓库中名叫requirements.txt
的文件。在之前的步骤中,我们已经使用git
命令成功地将Github仓库"克隆"到了/root/blog
目录;这个目录碰巧也是我们创建Dockerfile
的目录。这很重要,因为这意味着Dokcer在构建过程中可以访问Git仓库中的内容。
当我们执行构建后,Docker将构建的上下文环境设置为指定的"构建目录"。这意味着目录中的所有文件都可以在构建过程中被使用,目录之外的文件(构建环境之外)是不能访问的。
为了能安装需要的Python模块,我们需要将requirements.txt
从构建目录拷贝到容器中。我们可以在Dockerfile
中使用COPY
指令完成这一需求。
## Dockerfile that generates an instance of http://bencane.com
FROM nginx:latest
MAINTAINER Benjamin Cane <ben@bencane.com>
## Install python and pip
RUN apt-get update
RUN apt-get install -y python-dev python-pip
## Create a directory for required files
RUN mkdir -p /build/
## Add requirements file and run pip
COPY requirements.txt /build/
RUN pip install -r /build/requirements.txt
在Dockerfile
中,我们增加了3条指令。第一条指令使用RUN
在容器中创建了/build/
目录。该目录用来拷贝生成静态HTML页面需要的一切应用文件。第二条指令是COPY
指令,它将requirements.txt
从"构建目录"(/root/blog
)拷贝到容器中的/build/
目录。第三条使用RUN
指令来执行pip
命令;安装requirements.txt
文件中指定的所有模块。
当构建定制镜像时,COPY
是条重要的指令。如果在Dockerfile中不指定拷贝文件,Docker镜像将不会包含requirements.txt文件。在Docker容器中,所有东西都是隔离的,除非在Dockerfile中指定执行,否则容器中不会包括需要的依赖。
重新运行构建
现在,我们让Docker执行了一些定制任务,现在我们尝试另一次blog镜像的构建。
# docker build -t blog /root/blog
Sending build context to Docker daemon 19.52 MB
Sending build context to Docker daemon
Step 0 : FROM nginx:latest
---> 9fab4090484a
Step 1 : MAINTAINER Benjamin Cane <ben@bencane.com>
---> Using cache
---> 8e0f1899d1eb
Step 2 : RUN apt-get update
---> Using cache
---> 78b36ef1a1a2
Step 3 : RUN apt-get install -y python-dev python-pip
---> Using cache
---> ef4f9382658a
Step 4 : RUN mkdir -p /build/
---> Running in bde05cf1e8fe
---> f4b66e09fa61
Removing intermediate container bde05cf1e8fe
Step 5 : COPY requirements.txt /build/
---> cef11c3fb97c
Removing intermediate container 9aa8ff43f4b0
Step 6 : RUN pip install -r /build/requirements.txt
---> Running in c50b15ddd8b1
Downloading/unpacking jinja2 (from -r /build/requirements.txt (line 1))
Downloading/unpacking PyYaml (from -r /build/requirements.txt (line 2))
<truncated to reduce noise>
Successfully installed jinja2 PyYaml mistune markdown MarkupSafe
Cleaning up...
---> abab55c20962
Removing intermediate container c50b15ddd8b1
Successfully built abab55c20962
上述输出所示,我们可以看到构建成功了,我们还可以看到另外一个有趣的信息 ---> Using cache
。这条信息告诉我们,Docker在构建该镜像时使用了它的构建缓存。
Docker构建缓存
当Docker构建镜像时,它不仅仅构建一个单独的镜像;事实上,在构建过程中,它会构建许多镜像。从上面的输出信息可以看出,在每一"步"执行后,Docker都在创建新的镜像。
Step 5 : COPY requirements.txt /build/
---> cef11c3fb97c
上面片段的最后一行可以看出,Docker在告诉我们它在创建一个新镜像,因为它打印了Image ID;cef11c3fb97c
。这种方式有用之处在于,Docker能在随后构建blog镜像时将这些镜像作为缓存使用。这很有用处,因为这样,Docker就能加速同一个容器中新构建任务的构建流程。从上面的例子中,我们可以看出,Docker没有重新安装python-dev
和python-pip
包,Docker则使用了缓存镜像。但是由于Docker并没有找到执行mkdir
命令的构建缓存,随后的步骤就被一一执行了。
Docker构建缓存一定程度上是福音,但有时也是噩梦。这是因为使用缓存或者重新运行指令的决定在一个很狭窄的范围内执行。比如,如果requirements.txt
文件发生了修改,Docker会在构建时检测到该变化,然后Docker会重新执行该执行那个点往后的所有指令。这得益于Docker能查看requirements.txt
的文件内容。但是,apt-get
命令的执行就是另一回事了。如果提供Python包的Apt 仓库包含了一个更新的python-pip包;Docker不会检测到这个变化,转而去使用构建缓存。这会导致之前旧版本的包将被安装。虽然对python-pip
来说,这不是主要的问题,但对使用了某个致命攻击缺陷的包缓存来说,这是个大问题。
出于这个原因,抛弃Docker缓存,定期地重新构建镜像是有好处的。这时,当我们执行Docker构建时,我简单地指定--no-cache=True
即可。
部署博客的剩余部分
Python包和模块安装后,接下来我们将拷贝需要用到的应用文件,然后运行hamerkop
应用。我们只需要使用更多的COPY
and RUN
指令就可完成。
## Dockerfile that generates an instance of http://bencane.com
FROM nginx:latest
MAINTAINER Benjamin Cane <ben@bencane.com>
## Install python and pip
RUN apt-get update
RUN apt-get install -y python-dev python-pip
## Create a directory for required files
RUN mkdir -p /build/
## Add requirements file and run pip
COPY requirements.txt /build/
RUN pip install -r /build/requirements.txt
## Add blog code nd required files
COPY static /build/static
COPY templates /build/templates
COPY hamerkop /build/
COPY config.yml /build/
COPY articles /build/articles
## Run Generator
RUN /build/hamerkop -c /build/config.yml
现在我们已经写出了剩余的构建指令,我们再次运行另一次构建,并确保镜像构建成功。
# docker build -t blog /root/blog/
Sending build context to Docker daemon 19.52 MB
Sending build context to Docker daemon
Step 0 : FROM nginx:latest
---> 9fab4090484a
Step 1 : MAINTAINER Benjamin Cane <ben@bencane.com>
---> Using cache
---> 8e0f1899d1eb
Step 2 : RUN apt-get update
---> Using cache
---> 78b36ef1a1a2
Step 3 : RUN apt-get install -y python-dev python-pip
---> Using cache
---> ef4f9382658a
Step 4 : RUN mkdir -p /build/
---> Using cache
---> f4b66e09fa61
Step 5 : COPY requirements.txt /build/
---> Using cache
---> cef11c3fb97c
Step 6 : RUN pip install -r /build/requirements.txt
---> Using cache
---> abab55c20962
Step 7 : COPY static /build/static
---> 15cb91531038
Removing intermediate container d478b42b7906
Step 8 : COPY templates /build/templates
---> ecded5d1a52e
Removing intermediate container ac2390607e9f
Step 9 : COPY hamerkop /build/
---> 59efd1ca1771
Removing intermediate container b5fbf7e817b7
Step 10 : COPY config.yml /build/
---> bfa3db6c05b7
Removing intermediate container 1aebef300933
Step 11 : COPY articles /build/articles
---> 6b61cc9dde27
Removing intermediate container be78d0eb1213
Step 12 : RUN /build/hamerkop -c /build/config.yml
---> Running in fbc0b5e574c5
Successfully created file /usr/share/nginx/html//2011/06/25/checking-the-number-of-lwp-threads-in-linux
Successfully created file /usr/share/nginx/html//2011/06/checking-the-number-of-lwp-threads-in-linux
<truncated to reduce noise>
Successfully created file /usr/share/nginx/html//archive.html
Successfully created file /usr/share/nginx/html//sitemap.xml
---> 3b25263113e1
Removing intermediate container fbc0b5e574c5
Successfully built 3b25263113e1
运行定制的容器
成功的一次构建后,我们现在就可以通过运行docker
命令和run
选项来运行我们定制的容器,和之前我们启动nginx容器一样。
# docker run -d -p 80:80 --name=blog blog
5f6c7a2217dcdc0da8af05225c4d1294e3e6bb28a41ea898a1c63fb821989ba1
我们这次又使用了-d
(detach)标识来让Docker在后台运行。但是,我们也可以看到两个新标识。第一个新标识是--name
,这用来给容器指定一个用户名称。之前的例子,我们没有指定名称,因为Docker随机帮我们生成了一个。第二个新标识是-p
,这个标识允许用户从主机映射一个端口到容器中的一个端口。
之前我们使用的基础nginx镜像分配了80端口给HTTP服务。默认情况下,容器内的端口通道并没有绑定到主机系统。为了让外部系统能访问容器内部端口,我们必须使用-p
标识将主机端口映射到容器内部端口。上面的命令,我们通过-p 8080:80
语法将主机80端口映射到容器内部的80端口。
经过上面的命令,我们的容器似乎成功启动了,我们可以通过执行docker ps
核实。
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d264c7ef92bd blog:latest nginx -g 'daemon off 3 seconds ago Up 3 seconds 443/tcp, 0.0.0.0:80->80/tcp blog
总结
截止目前,我们拥有了正在运行的定制Docker容器。虽然在这篇文章中,我们只接触了一些Dockerfile指令用法,但是我们还是要讨论所有的指令。我们可以检查Docker's reference page来获取所有的Dockerfile指令用法,那里对指令的用法说明得很详细。
另一个比较好的资源是Dockerfile Best Practices page,它有许多构建定制Dockerfile的最佳练习。有些技巧非常有用,比如战略性地组织好Dockerfile中的命令。上面的例子中,我们将articles
目录的COPY
指令作为Dockerfile中最后的COPY
指令。这是因为articles
目录会经常变动。所以,将那些经常变化的指令尽可能地放在最后面的位置,来最优化那些可以被缓存的步骤。
通过这篇文章,我们涉及了如何运行一个预构建的容器,以及如何构建,然后部署定制容器。虽然关于Docker你还有许多需要继续学习的地方,但我想这篇文章给了你如何继续开始的好建议。当然,如果你认为还有一些需要继续补充的内容,在下面评论即可。
作者:Benjamin Cane
译者:su-kaiyao
校对:校对者ID