TranslateProject/translated/tech/20151210 Getting started with Docker by Dockerizing this Blog.md.md
2015-12-17 22:14:26 +08:00

26 KiB
Raw Blame History

通过Dockerize这篇博客来开启我们的Docker之旅

这篇文章将包含Docker的基本概念以及如何通过创建一个定制的Dockerfile来Dockerize一个应用 作者Benjamin Cane2015-12-01 10:00:00

Docker是2年前从某个idea中孕育而生的有趣技术世界各地的公司组织都积极使用它来部署应用。在今天的文章中我将教你如何通过"Dockerize"一个现有的应用来开始我们的Docker运用。问题中的应用指的就是这篇博客

什么是Docker

当我们开始学习Docker基本概念时让我们先去搞清楚什么是Docker以及它为什么这么流行。Docker是一个操作系统容器管理工具它通过将应用打包在操作系统容器中来方便我们管理和部署应用。

容器 vs. 虚拟机

容器虽和虚拟机并不完全相似,但它也是一种提供操作系统虚拟化的方式。但是,它和标准的虚拟机还是有不同之处的。

标准虚拟机一般会包括一个完整的操作系统,操作系统包,最后还有一至两个应用。这都得益于为虚拟机提供硬件虚拟化的管理程序。这样一来,一个单一的服务器就可以将许多独立的操作系统作为虚拟客户机运行了。

容器和虚拟机很相似,它们都支持在单一的服务器上运行多个操作环境,只是,在容器中,这些环境并不是一个个完整的操作系统。容器一般只包含必要的操作系统包和一些应用。它们通常不会包含一个完整的操作系统或者硬件虚拟化程序。这也意味着容器比传统的虚拟机开销更少。

容器和虚拟机常被误认为是两种抵触的技术。虚拟机采用同一个物理服务器,来提供全功能的操作环境,该环境会和其余虚拟机一起共享这些物理资源。容器一般用来隔离运行中的应用进程,运行进程将在单独的主机中运行,以保证隔离后的进程之间不能相互影响。事实上,容器和BSD Jails以及chroot进程的相似度,超过了和完整虚拟机的相似度。

Docker在容器的上层提供了什么

Docker不是一个容器运行环境事实上只是一个容器技术并不包含那些帮助Docker支持Solaris ZonesBSD 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部署预先构建好的容器的方式就像yumapt-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 nginxDocker就无需再次拉取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 updateapt-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-devpython-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-devpython-pipDocker则使用了缓存镜像。但是由于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你还有许多需要继续学习的地方但我想这篇文章给了你如何继续开始的好建议。当然如果你认为还有一些需要继续补充的内容在下面评论即可。


via:http://bencane.com/2015/12/01/getting-started-with-docker-by-dockerizing-this-blog/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+bencane%2FSAUo+%28Benjamin+Cane%29

作者Benjamin Cane

译者:su-kaiyao

校对:校对者ID

本文由 LCTT 原创翻译,Linux中国 荣誉推出