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而言是个好例子:
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里有:
COPY ./my-app/ /home/app/
RUN npm install # or RUN pip install or RUN bundle install
意味每次修改文件,都得重建那行以下的所有东西。多数情况下(包括上面的例子),这意味着得重新安装应用依赖。为了尽可能地使用Docker的缓存,先复制所有安装依赖所需要的文件,然后执行命令安装这些依赖。在复制剩余文件之前先做这两个步骤(在最后一行),将使变更被快速的重建
COPY ./my-app/package.json /home/app/package.json # Node/npm packages
WORKDIR /home/app/
RUN npm install
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)中,迁移是不能进行的。
COPY /YOUR-PROJECT /YOUR-PROJECT
RUN python manage.py migrate
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
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:
FROM node:6.2.1
FROM python:3.5
CMD ["sleep", "infinity"]
那么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 (翻译:陈晏娥)
===========================================
译者介绍
陈晏娥,鞍钢集团矿业公司信息开发中心运维高级工程师,专注虚拟化技术。