天天看点

九个编写Dockerfiles的常见错误或许还要安装python依赖?

本文讲的是<b>九个编写Dockerfiles的常见错误</b>编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是对于初级到中级开发者,该文章将会是一份很有用的指南,它有助于理清以及加速你们的工作流程。

执行<code>apt-get install</code>是每一个Dockerfile都有的东西之一。你需要安装一些外部的包来运行代码。但使用<code>apt-get</code>相应地会带来一些问题。

一个是运行<code>apt-get upgrade</code> 会更新所有包到最新版本 —— 不能这样做的理由是它会妨碍Dockerfile构建的持久与一致性。

另一个是在不同的行之间运行<code>apt-get update</code>与<code>apt-get install</code>命令。不能这样做的原因是,只有<code>apt-get update</code>的代码会在构建过程中被缓存,而且你需要运行<code>apt-get install</code>命令的时候不会每次都被执行。因此,你需要将<code>apt-get update</code>跟所要安装的包都在同一行执行,来确保它们正确的更新。

<code>ADD</code>与<code>COPY</code>是完全不同的命令。<code>COPY</code>是这两个中最简单的,它只是从主机复制一份文件或者目录到镜像里。<code>ADD</code>同样可以这么做,但是它还有更神奇的功能,像解压TAR文件或从远程URLs获取文件。为了降低Dockerfile的复杂度以及防止意外的操作,最好用<code>COPY</code>来复制文件。

明确代码的哪些部分以及什么时候应该放在构建镜像内或许是最重要的事了,它可以显著加快构建速度。

Dockerfile里经常会看到如下这些内容:

这就意味着每次修改文件之后都需要重新构建那行以下的所有东西。多数情况下(包括上面的例子),它意味着重新安装应用依赖。为了尽可能地使用Docker的缓存,首先复制所有安装依赖所需要的文件,然后执行命令安装这些依赖。在复制剩余文件(这一步尽可能放到最后一行)之前先做这两个步骤,会使代码的变更被快速的重建。

COPY ./my-app/requirements.txt /home/app/requirements.txt

RUN pip install -r requirements.txt

COPY ./my-app/ /home/app/

这样做会确保构建尽可能快的执行。

许多Dockerfiles在开头都使用<code>FROM node:latest</code>模板,用来从Docker registry拉取最新的镜像。简单地说,使用<code>latest</code>标签的镜像意味着如果这个镜像得到更新,那么Dockerfile的构建可能会突然中断。弄清这件事可能会非常难,因为Dockerfile的维护者实际上并没做任何修改。为了防止这种情况,只需要确保镜像使用特定的标签(例如:<code>node:6.2.1</code>)。这样就可以确保Dockerfile的一致性。

很多人会忽视构建Docker镜像与运行一个Docker容器的区别。在构建镜像时,Docker读取Dockerfile里的命令并创建镜像。在依赖或代码修改之前,镜像是保持不变以及可重复使用的。这个过程完全独立于其它容器。需要与其它容器或服务(如数据库)进行交互则会在容器运行的时候发生。

举一个例子,执行数据库迁移。很多人试图在构建镜像时执行此操作。这样做会导致许多问题。首先,在构建时数据库可能不可用,因为它可能没建在它将要运行的服务器上。其次,你可能想使用同一个镜像来连接不同的数据库(在开发或生产环境中),在这种情况下,如果它在构建过程中,迁移是不能进行的。

EXPOSE和ENV是廉价的执行命令。如果你破坏它们的缓存,几乎瞬时就可以重建。所以,最好尽可能晚地声明这些命令。在构建过程中应该直到需要的时候才声明ENV。如果在构建的时候不需要他们,那么应该在Dockerfile的末尾附加<code>EXPOSE</code>。

再次查看Golang的Dockerfile,你会看到,所有<code>ENVS</code>都是在使用前声明的,并且在最后声明其余的:

如需修改<code>ENV GOPATH</code>或<code>ENV PATH</code>,镜像几乎会马上重建成功。

尝试使用多个<code>FROM</code>声明来将不同的镜像组合到一起,这样不会起任何作用。Docker仅使用最后一个<code>FROM</code>并且忽略前面所有的。

所以如果你有这样的Dockerfile:

那么<code>docker exec</code>进入运行的容器中,会得到下面的结果:

这其实是GitHub上的一个问题:合并不同的镜像,但它看起来不会很快就增加的功能。

这可能是了解Docker的开发者遇到的最大问题。而公认的最佳实践是:每个不同的服务,包括应用,应该在它自己的容器中运行。在一个Docker镜像里面加入多个服务非常容易,但是有一定的负面影响。

首先,横向扩展应用会变得很困难。其次,额外的依赖和层次会使镜像构建变慢。最终,增大了Dockerfile的编写、维护以及调试难度。

当然,像所有的技术建议一样,你需要用你的最佳判断。如果想快速安装一个<code>Django</code>+<code>Nginx</code>的应用的开发环境,那么让它们运行在同一个容器里面,同时生产环境中有一个不同的Dockerfile,让他们分开运行,是合理可行的。

<code>Volume</code>是在运行容器时候加入的,而不是构建的时候。与第五个误区类似,在构建过程中不应该与你声明的<code>volume</code>有交互。相反地,你只是在运行容器的时候使用它。例如,如果在以下构建过程中创建文件并且在运行那个镜像时候使用它,一切正常:

但是,如果我对一个存储在<code>volume</code>上的文件做同样的事,就不会起作用。

一个有趣的问题是:如果你前面的任何一个层次声明了一个<code>VOLUME</code>(也可能是几个<code>FROMS</code>)依然会遇到同样的问题。因此,最好留意一下父类镜像都声明了什么<code>volume</code>。如果遇到问题,请使用<code>docker inspect</code>检查。

理解怎样写好一个<code>Dockerfile</code>将会是一个漫长的路程,它会带你理解<code>Docker</code>是如何工作的,同时也帮助你建立你的基础架构。理解Docker缓存会为你节省好多等待构建完成的时间!

原文发布时间为:2016-06-16

本文来自云栖社区合作伙伴Dockerone.io,了解相关信息可以关注Dockerone.io。

原文标题:九个编写Dockerfiles的常见错误