本文讲的是九个编写Dockerfiles的常见错误编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是对于初级到中级开发者,该文章将会是一份很有用的指南,它有助于理清以及加速你们的工作流程。
一个是运行
另一个是在不同的行之间运行
在以下 Golang Dockerfile 中
Dockerfile里经常会看到如下这些内容:
这就意味着每次修改文件之后都需要重新构建那行以下的所有东西。多数情况下(包括上面的例子),它意味着重新安装应用依赖。为了尽可能地使用Docker的缓存,首先复制所有安装依赖所需要的文件,然后执行命令安装这些依赖。在复制剩余文件(这一步尽可能放到最后一行)之前先做这两个步骤,会使代码的变更被快速的重建。
这样做会确保构建尽可能快的执行。
举一个例子,执行数据库迁移。很多人试图在构建镜像时执行此操作。这样做会导致许多问题。首先,在构建时数据库可能不可用,因为它可能没建在它将要运行的服务器上。其次,你可能想使用同一个镜像来连接不同的数据库(在开发或生产环境中),在这种情况下,如果它在构建过程中,迁移是不能进行的。
再次查看Golang的Dockerfile,你会看到,所有
如需修改
所以如果你有这样的Dockerfile:
那么
这其实是GitHub上的一个问题:合并不同的镜像,但它看起来不会很快就增加的功能。
首先,横向扩展应用会变得很困难。其次,额外的依赖和层次会使镜像构建变慢。最终,增大了Dockerfile的编写、维护以及调试难度。
当然,像所有的技术建议一样,你需要用你的最佳判断。如果想快速安装一个
但是,如果我对一个存储在
一个有趣的问题是:如果你前面的任何一个层次声明了一个
原文链接:9 Common Dockerfile Mistakes (翻译:陈晏娥 校对:田浩浩 )
1. 执行 apt-get
执行apt-get install
是每一个Dockerfile都有的东西之一。你需要安装一些外部的包来运行代码。但使用
apt-get
相应地会带来一些问题。
一个是运行
apt-get upgrade
会更新所有包到最新版本 —— 不能这样做的理由是它会妨碍Dockerfile构建的持久与一致性。
另一个是在不同的行之间运行
apt-get update
与
apt-get install
命令。不能这样做的原因是,只有
apt-get update
的代码会在构建过程中被缓存,而且你需要运行
apt-get install
命令的时候不会每次都被执行。因此,你需要将
apt-get update
跟所要安装的包都在同一行执行,来确保它们正确的更新。
在以下 Golang Dockerfile 中
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. 在一行内添加整个应用目录
明确代码的哪些部分以及什么时候应该放在构建镜像内或许是最重要的事了,它可以显著加快构建速度。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
或许还要安装python依赖?
COPY ./my-app/requirements.txt /home/app/requirements.txt RUN pip install -r requirements.txt COPY ./my-app/ /home/app/这样做会确保构建尽可能快的执行。
4. 使用:latest标签
许多Dockerfiles在开头都使用FROM node:latest
模板,用来从Docker registry拉取最新的镜像。简单地说,使用
latest
标签的镜像意味着如果这个镜像得到更新,那么Dockerfile的构建可能会突然中断。弄清这件事可能会非常难,因为Dockerfile的维护者实际上并没做任何修改。为了防止这种情况,只需要确保镜像使用特定的标签(例如:
node:6.2.1
)。这样就可以确保Dockerfile的一致性。
5. 构建镜像时使用外部服务
很多人会忽视构建Docker镜像与运行一个Docker容器的区别。在构建镜像时,Docker读取Dockerfile里的命令并创建镜像。在依赖或代码修改之前,镜像是保持不变以及可重复使用的。这个过程完全独立于其它容器。需要与其它容器或服务(如数据库)进行交互则会在容器运行的时候发生。举一个例子,执行数据库迁移。很多人试图在构建镜像时执行此操作。这样做会导致许多问题。首先,在构建时数据库可能不可用,因为它可能没建在它将要运行的服务器上。其次,你可能想使用同一个镜像来连接不同的数据库(在开发或生产环境中),在这种情况下,如果它在构建过程中,迁移是不能进行的。
# !!! ANTIPATTERN !!! COPY /YOUR-PROJECT /YOUR-PROJECT RUN python manage.py migrate # 尝试迁移数据,但是并不能 CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] # !!! ANTIPATTERN !!!
6. 在Dockerfile开始部分加入EXPOSE和ENV
EXPOSE和ENV是廉价的执行命令。如果你破坏它们的缓存,几乎瞬时就可以重建。所以,最好尽可能晚地声明这些命令。在构建过程中应该直到需要的时候才声明ENV。如果在构建的时候不需要他们,那么应该在Dockerfile的末尾附加EXPOSE
。
再次查看Golang的Dockerfile,你会看到,所有
ENVS
都是在使用前声明的,并且在最后声明其余的:
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
,镜像几乎会马上重建成功。
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镜像里面加入多个服务非常容易,但是有一定的负面影响。首先,横向扩展应用会变得很困难。其次,额外的依赖和层次会使镜像构建变慢。最终,增大了Dockerfile的编写、维护以及调试难度。
当然,像所有的技术建议一样,你需要用你的最佳判断。如果想快速安装一个
Django
+
Nginx
的应用的开发环境,那么让它们运行在同一个容器里面,同时生产环境中有一个不同的Dockerfile,让他们分开运行,是合理可行的。
9. 在构建过程中使用VOLUME
Volume
是在运行容器时候加入的,而不是构建的时候。与第五个误区类似,在构建过程中不应该与你声明的
volume
有交互。相反地,你只是在运行容器的时候使用它。例如,如果在以下构建过程中创建文件并且在运行那个镜像时候使用它,一切正常:
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
(也可能是几个
FROMS
)依然会遇到同样的问题。因此,最好留意一下父类镜像都声明了什么
volume
。如果遇到问题,请使用
docker inspect
检查。
结论
理解怎样写好一个Dockerfile
将会是一个漫长的路程,它会带你理解
Docker
是如何工作的,同时也帮助你建立你的基础架构。理解Docker缓存会为你节省好多等待构建完成的时间!
原文链接:9 Common Dockerfile Mistakes (翻译:陈晏娥 校对:田浩浩 )
原文发布时间为:
2016-06-16
本文来自云栖社区合作伙伴Dockerone.io,了解相关信息可以关注Dockerone.io。
原文标题:九个编写Dockerfiles的常见错误