转载

翻译:9个常见Dockerfiles错误

9个常见Dockerfiles错误

【编者的话】我们每天基于Dockerfiles工作;所有运行的代码都基于一系列的Dockerfiles。我们在这篇文章里,讨论人们经常犯的错误和怎样改进代码。这篇文章里的许多技巧,Docker专家可能会非常清楚和认同。期望这篇文章对初学者到中级开发者,是一个有用的指南,能够有助于澄清问题和加速你们的工作流程。

1.运行apt-get

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

第一个是运行apt-get upgrade。会更新所有包到它们的最新版本——这是不好的,会阻止你的Dockerfile创建一致、持久的生成(build)。

另一个是在不同的行中运行apt-get update而不是运行apt-get install命令。这是不好的,原因是,一行只有apt-get update的代码将在生成(build)的时候缓存,不会在你每次需要运行apt-get installd的时候都被实际运行。相反,需要确保所有的包都运行同一行apt-get update,来确保它们更新正确。

Golang Dockerfile(来自 http://t.umblr.com/redirect%3F ... %253D )对于命令apt-install而言是个好例子:

From https://github.com/docker-library/golang

RUN apt-get update && /

apt-get install -y --no-install-recommends /

g++ /

gcc /

libc6-dev /

make /

&& rm -rf /var/lib/apt/lists/*

2.使用ADD而不是COPY

ADD和COPY是完全不同的命令。COPY是这两个里最简单的,它只是从主机到镜像复制一个文件或目录。ADD也能做这个,但还有更神奇的功能,像解压TAR文件或从远程URLs获取文件。为了降低Dockerfile的复杂度和预防非预期的操作,最好用COPY来复制文件。

FROM busybox:1.24

ADD example.tar.gz /add #解压缩文件到ADD目录

COPY example.tar.gz /copy #直接复制文件

3.在一行里添加整个应用目录。

明确:你代码的哪部分、在什么时间,将被包括进你的生成(build),是可以显著加快生成(build)速度最重要的事。

经常,一个Dockerfile里有:

!!! ANTIPATTERN !!!

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

RUN npm install # or RUN pip install or RUN bundle install

!!! ANTIPATTERN !!!

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

COPY ./my-app/package.json /home/app/package.json # Node/npm packages

WORKDIR /home/app/

RUN npm install

Maybe you have to install python packages too?

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

RUN pip install -r requirements.txt

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

这会确保你的构建(builds)尽可能快的运行。

4.使用:latest

一些Dockerfiles在最上面使用FROM node:latest模板来从Docker registry拉取最新的镜像。简单地说,给一个镜像使用latest标签意味着如果这个镜像得到更新,你的生成(build)可能会突然中断。弄清这件事可能会非常难,因为Dockerfile维护者实际上没做任何的改变。为了防止这种情况,只要确保你给镜像使用特定标签(例如:node:6.2.1)。将确保Dockerfile保持不变。

5.生成(build)时使用外部服务

很多人忘了生成(build)一个Docke镜像和运行一个Docker容器的区别。在生成(build)镜像时,Docker读取Dockerfile里的命令并从它创建镜像。在依赖或代码改变前,镜像将是一成不变,可重复使用的。这个过程完全独立于其它容器。任何与其它容器或服务(如数据库)进行交互的事情,将在容器运行的时候发生。

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

!!! ANTIPATTERN !!!

COPY /YOUR-PROJECT /YOUR-PROJECT

RUN python manage.py migrate

runserver would actually try to the migration, but imagine it doesn’t

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

!!! ANTIPATTERN !!!

6.在Dockerfile前面加入EXPOSE和ENV

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

再一次看Go Dockerfile,你会看到,所有ENV都是在使用前声明的,并且在最后声明其余的:

ENV GOLANG_VERSION 1.7beta1

ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go $GOLANG_VERSION.linux-amd64.tar.gz

ENV GOLANG_DOWNLOAD_SHA256 a55e718935e2be1d5b920ed262fd06885d2d7fc4eab7722aa02c205d80532e3b

RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz /

&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - /

&& tar -C /usr/local -xzf golang.tar.gz /

&& rm golang.tar.gz

ENV GOPATH /go

ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH

如果需要改变ENV GOPATH或ENV PATH,镜像几乎会马上重新生成(build)。

7.多FROM声明

尝试使用多FROM声明将不同的镜像组合到一起,是不会起作用的。Docker仅使用最后一个FROM而忽略前面所有的。

所以如果你有这样的Dockerfile:

!!! ANTIPATTERN !!!

FROM node:6.2.1

FROM python:3.5

CMD ["sleep", "infinity"]

!!! ANTIPATTERN !!!

那么docker exec进入运行的容器,是下面的:

$ docker exec -it d86fcf0775d3 bash

root@d86fcf0775d3:/# which python

/usr/local/bin/python

root@d86fcf0775d3:/# which node

root@d86fcf0775d3:/#

这其实是GitHub上的一个问题,组合不同镜像到一起,但它看起来不像一个能够被很快增加的功能。

8.多个服务运行在同一个容器里

这可能是懂Docker者的最大问题。这个是公认的最佳实践:每个不同的服务,包括应用,应该在它自己的容器中运行。在一个Docker镜像里面加入多个服务是很有诱惑力的,但是有一定的负面影响。

首先,横向扩展应用会变得困难。其次,额外的依赖和层次会让生成(build)变慢。最后,增加了Dockerfile的编写、维护和调试的难度。

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

9.在生成(build)过程中使用VOLUME

Volume是在运行容器时候加入的,而不是生成(build)的时候。类似的对于#5,你不应该与你声明的volume在生成(build)的过程中互动。相反地,你应该在运行容器时只是使用它。例如,如果我在生成(build)过程中创建一个文件并且在运行那个镜像时候使用它,一切正常:

FROM busybox:1.24

RUN echo "hello-world!!!!" > /myfile.txt

CMD ["cat", "/myfile.txt"]

$ docker run volume-in-build

hello-world!!!!

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

FROM busybox:1.24

VOLUME /data

RUN echo "hello-world!!!!" > /data/myfile.txt

CMD ["cat", "/data/myfile.txt"]

$ docker run volume-in-build

cat: can't open '/data/myfile.txt': No such file or directory

一个有趣的问题是:如果任何你前面的层次声明了一个VOLUME(可能是机个FROM以前)你会依然遇到同样的问题。因此,留意你的祖先镜像声明了什么volume是一个好主意。如果遇到问题,使用docker inspect。

结论

理解怎样写一个好的Dockerfile有很长的路,将带你理解Docker是怎样工作的,同时也帮助你建立抽象的基础。理解Docker缓存将为你节省好多等待生成(build)完成的时间!

原文链接: http://blog.runnable.com/post/ ... takes (翻译:陈晏娥)

===========================================

译者介绍

陈晏娥,鞍钢集团矿业公司信息开发中心运维高级工程师,专注虚拟化技术。

  • 9个常见Dockerfiles错误.pdf
原文  http://dockone.io/article/1414
正文到此结束
Loading...