让博客Docker化,轻松上手Docker

Docker是一个有趣的技术,在过去的两年已经从一个想法变成了全世界的机构都在采用来部署应用的技术。在今天的文章中我将会讨论如何通过将一个现有应用Docker化来上手Docker。那这里选取的现有应用就是我的博客。
什么是Docker

在我们开始学习Docker的基础知识之前让我们首先理解什么是Docker,并且为什么它那么流行。Docker是一个操作系统容器管理工具,通过将应用打包到操作系统容器里面,从而让你能轻松管理和部署应用。

容器 vs. 虚拟机
容器可能不如虚拟机一样为人所熟知,但是它们是另外的一种提供操作系统虚拟化的方法。然而,他们与标准的虚拟机有很大的差异。

标准的虚拟机通常包含一个完整的操作系统,OS软件包,最后包含一两个应用。它是通过一个向虚拟机提供了硬件虚拟化的Hypervisor来实现的,允许单个服务器运行很多独立的被当做虚拟游客(virtual guest)的操作系统。

而容器与虚拟机的类似之处在于它们允许单个服务器运行多个操作环境(operating environment),然而这些环境不却是完整的操作系统。容器通常只包含必要的OS软件包和应用。他们通常不包含一个完整的操作系统或者硬件虚拟化。这也意味着比之虚拟机,容器的额外开销(overhead)更小。

容器和虚拟机通常被视为不能共生的技术,然而这通常是一个误解。虚拟机面向物理服务器,提供可以能与其他虚拟机一起共享这些物理资源的,功能完善的操作环境。容器通常是用来通过对单一主机的一个进程进行隔离,来保证被隔离的进程无法与处于同一个系统的其他进程进行互动。实际上,比起完全的虚拟机,容器与BSD的Jail,chroot的进程更加类似。

在容器的基础上Docker提供了什么
Docker自身并不是一个容器的运行时环境;实际上Docker实际上是对容器技术不可知的(container technology agnostic),并且为了支持Solaris Zones和BSD Jails花了不少功夫。Docker提供的是一种容器管理,打包和部署的方法。尽管这种类型的功能已经某一种程度地存在于虚拟机中,但在传统上,它们并不是为了绝大多数的容器方案而生的,而那些已经存在的,却又不如Docker一样容易使用且功能完善。

现在我们知道了Docker是什么,让我们开始通过安装Docker并且部署一个公共的预先构建好的容器来学习Docker是如何工作的。

从安装开始
因为Docker不会默认安装好,第一步就是安装Docker软件包;因为我们实例中使用的操作系统是Ubuntu 14.04,我们将会使用Apt包管理工具:

apt-get install docker.ioReading 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 psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

docker命令的ps功能类似于Linux的ps命令。它能显示可用的Dokcer容器和他们的当前状态。因为我们还没有启动任何的Docker容器,命令显示没有任何的正在运行的容器。

部署一个预先构建好的nginx Docker容器
Docker的一个我最喜欢的特性是其可有让你用类似yum或者apt-get部署一个软件包一样的方式来部署一个预先构建好容器的能力。为了更好的说明这一点,让我们来部署一个预先构建好的运行nginx服务器的容器。我们可以通过执行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会绑定到这个容器的console和在容器里面运行的进程。为了在将这个Docker容器在后台启动,我包含了一个-d(detach,脱离)的标志。

现在再次运行docker ps,我们可以看到正在运行的nginx容器:

docker psCONTAINER 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构建而来。

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首先检查是否本地已经存在有一个该特定名称的镜像。

因为我们的系统是全新的,没有一个名为nginx的Docker镜像,这意味着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这么简单。在这篇文章中,我们不会部署一个自己的注册表服务(registry service)。

停止和移除镜像
在我们开始构建一个自己的Docker容器之前,让我们首先清理我们的Docker环境。我们需要停止之前启动的容器并且移除它。

要启动一个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,容器将会立即=启动而不需要再次拉取名叫nginx的镜像。这是因为Docker已经在本地保存了一个备份。

要查看所有本地的镜像我们可以是运行docker命令并且使用image选项。

docker images

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE

nginx latest 9fab4090484a 5 days ago 132.8 MB

构建我们自己的自定义镜像

到目前为止我们已经使用了一些基础的Docker命令来启动,停止和移除一个普通的预选构建好的镜像。而为了Docker化这个博客,我们将要构建我们自己的Docker镜像,这意味要创建一个Dockerfile。

在绝大多数的虚拟机环境中,假如你想要创建一个机器的镜像,你需要首先创建一个虚拟机,然后安装好操作系统,然后安装好应用程序,最后将其转化成一个模板或者镜像。然而,对于Docker来说,这些步骤都可以通过Dockerfile进行自动化。一个Dockerfile是一个可以向Docker提供构建指令的方式。在这一节中,我们将要创建一个可以用来部署本博客的自定义Dockerfile。

理解应用
在我们开始创建Dockerfile之前,我们需要首先理解要部署这个博客我们必需什么。

这个博客自身实际上是通过一个自己编写的名为hamerkop(注:锤头鹳的意思)的静态网站生成器生成的一个静态的HTML网页。这个生成器非常的简单,是我专门为这个博客编写的,刚好够用。这个博客的所有的代码和源文件都能在公有的Github仓库中访问到。为了部署这个博客我们只需要从Github拿到该仓库的内容,然后安装Python和一些Python的模块,并且执行hamerkop应用。要对外服务这些生成的内容我们需要使用nginx;这意味着我们也需要安装好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镜像指定为基础镜像。这基本上为我们提供了继承另一个Docker镜像的方法。在我们这个场景中,我们将会从我们之前用到的nginx镜像开始,如果我们想要从一个最原始的空白状态(* blank slate*)开始,我们可以通过指定ubuntu:latest使用Ubuntu镜像。

FROM nginx:latest

MAINTAINER Benjamin Cane

除了FROM指令之外,我还包含了一个MAINTAINER指令,其是用来显示Dockerfile的作者的。
因为Docker支持使用#来作为评论的标示,我将会使用这种语法来解释Dockerfile的各个部分。

运行一个测试构建
因为我们继承了nginx Docker镜像,我们当前的Dockerfil也继承了所有用来构建该nginx镜像的所有指令。这意味着即使在这一刻,我们已经能够从这个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

—> Running in c97f36450343

—> 60a44f78d194

Removing intermediate container c97f36450343

Successfully built 60a44f78d194

在上面的例子中,我们使用-t(tag,标签)标志来将这个镜像贴上名为"blog"的标签。这基本上让我们可以对镜像进行命名。假如不为镜像指定一个标签,这个镜像就只能通过一个由Dokcer指定的镜像ID(Image ID)来调用。在这个场景下,这镜像ID是60a44f78d194,正如我们在docker命令的成功构建消息中看到的一样。

除了-t标志,我也指定了/root/blog目录。这个目录就是“构建目录(build directory)”,这个目录包含了Dockerfile和其他必要的构建这个容器的文件。

现在我们已经完成了一个成功的构建,让我们开始对这个镜像进行定制化。

使用RUN来执行apt-get
这个用来生成HTML页面的静态网站生成器是使用Python来编写的,因此在Dockerfile中第一个自定义的任务是安装Python。要安装Python包,我们需要用到Apt包管理器,这意味着我们需要在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 Install python and pipRUN apt-get update

RUN apt-get install -y python-dev python-pip

在上面我们仅仅是运行RUN指令来告诉Docker当其构建这个镜像的时候,它需要执行指定的apt-get命令。然而有意思的部分是,这些命令只会在这个容器的情景(context)中才会执行。这意味着python-dev和python-pip只被安装到了容器中,并没有安装到主机中。或者用更简单的话来说,在容器中,pip是可以执行的,但是出了容器之外,pip命令是找不到的。

同样重要的一点是,Docker的构建过程中是不接受用户的输入的。这意味这所有由RUN指令来执行的命令必须不藉由用户输入而完成。这给构建过程增加了一点复杂度,因为很多应用是需要用户输入的。在我们这个场景中,RUN所有执行的命令都不需要用户输入。

安装Python模块

现在Python安装好了我们需要安装一些Python模块。要在Docker之外做这件事情,我们通常是使用pip命令并且引用在博客的仓库中的一个名叫requirements.txt文件。在之前的一个步骤里,我们使用了git命令将博客的Github仓库克隆到/root/blog目录下;这个目录也同时是我们创建Dockerfile的地方。这很重要,因为这意味着Git仓库的内容能被Docker在构建过程中访问到。

当执行构建的时候,Docker会将构建的情景(context)设置为一个指定的构建目录。这意味该文件夹任何的文件以及子目录中的文件都能被构建过程所使用,而处于该目录之外(处于构建情景之外的),是不能被访问到的。

要安装必需的Python模块,我们需要将requirements.txt文件从构建目录拷贝到容器之中去。我们可以在Dockerfile中使用COPY指令。

Dockerfile that generates an instance of http://bencane.com

FROM nginx:latest MAINTAINER Benjamin Cane Install python and pipRUN apt-get update

RUN apt-get install -y python-dev python-pipCreate a directory for required filesRUN mkdir -p /build/Add requirements file and run pipCOPY requirements.txt /build/

RUN pip install -r /build/requirements.txt

在Dockerfile中我们添加了3个指令。第一个指令使用RUN在容器中创建了一个/build目录。这个目录将用来拷贝用来生成静态HTML页面所需的任何文件。第二个指令是`COPY指令,用来将requirements.txt从构建目录拷贝到容器中的/build目录。第三个使用了RUN指令,用来执行pip命令;这会安装所有在requirements.txt文件中指定的模块。

COPY是一个在构建定制化的镜像时需要理解的很重要的指令。没有在Dockerfile文件中指定复制文件,Docke镜像就不会包含这个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

—> 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))

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在告诉我们创建了一个新的镜像,它通过输出镜像ID来告诉我们这一点;cef11c3fb97c。这个策略的一个有用之处在于Docker能够使用这些镜像作为后续构建步骤的缓存。这很有用,因为它能让Docker加快相同容器的新构建的构建过程。如果仔细我们看上面的例子,我们可以发现Docker能够使用一个已经缓存了的镜像,而不是重新安装python-dev和python-pip包。然而,因为Docker无法找到一个执行过mkdir命令的构建,这之后每一个后续的步骤都执行了。

Docker的构建缓存是一个馈赠也是一个诅咒;这么说的原因是否要使用缓存或者重新执行指令这个决定是在一个很狭窄的范围做出的。比如,如果如果有对requirements.txt文件的更改,Docker会在构建过程中检测到这个改动然后从那一点重新开始。然而执行apt-get命令却情况不同。如果提供Python包的Apt仓库包含了一个python-pip包更新的版本;Docker无法检测到这个变化,然后简单地使用缓存。这意味着可能我们安装了软件包的一个较老的版本。尽管这个对于python-pip软件包来说这不是什么问题,如果安装包缓存了一个包含已知漏洞的软件包,那么就是一个问题。

介于这个原因,周期性的重新构建镜像并且不使用Docker的缓存是有用的。你可以在执行一个Docker构建的时候指定–no-cache=True来禁用缓存。

部署blog的其余部分
当Python软件包和模块都安装好后,现在我们应该拷贝必需的应用文件,然后运行hamerkop应用了。要完成这一步我们可以简单地使用更多的COPY和RUN指令。

FROM nginx:latest

MAINTAINER Benjamin Cane 安装python和pipRUN apt-get update

RUN apt-get install -y python-dev python-pip创建一个文件夹放置必需文件RUN mkdir -p /build/添加依赖文件然后运行pipCOPY requirements.txt /build/

RUN pip install -r /build/requirements.txt添加博客代码和必需文件COPY static /build/static

COPY templates /build/templates

COPY hamerkop /build/

COPY config.yml /build/

COPY articles /build/articles运行生成器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

—> 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

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服务。默认情况下,与Docker容器内部绑定的端口并没有与主机系统绑定。为了让外部的系统访问容器内部暴露的端口,这些端口必须通过使用-p标志从主机端口映射到容器端口。假如我们想要端口从主机的8080端口,映射到容器中的80端口,我们可以通过使用这种语法-p 8080: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的手册页面,那里很好的解释了每个指令。

另外一个很好的资源是Docker最佳实践页面,包含了不少的构建自定制Dockerfile最佳实践。有些指点十分有用,比如策略性地在Dockerfile中安排指令的顺序。在上面的例子中,我们的Dockerfile用到的COPY指令,被放在了最后。这么做的原因是,articles目录会频繁更改。最好将可能经常变动的指令放到最后,这样可以优化Docker的缓存步骤。

在这个文章中我覆盖了如何从一个预先构建好的容器开始,如何构建然后部署一个自定制的容器。尽管Docker要学习的内容还有很多,希望这篇文章会助你迈出第一步。

猜你喜欢

转载自blog.csdn.net/liqiuman180688/article/details/84643135