2016 年 10 月 5 日

九个编写 Dockerfiles 的常见错误

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

1. 执行 apt-get

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

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

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

在以下 Golang Dockerfileapt-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

ADDCOPY是完全不同的命令。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 GOPATHENV 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缓存会为你节省好多等待构建完成的时间!

最新文章

  1. 网站优化怎样避免搜索引擎蜘蛛陷阱
  2. 北京网站建设一定要注意的四大要点
  3. 网站优化过程要小心这些雷区
  4. 网站优化过度要如何解决?教你几招!
  5. 网站建设不能忽视的四个问题
  6. 移动端网站建设需要注意的问题有哪些
  7. 一个网站设计需要多长时间
  8. 北京网站设计:中国和西方B2B网站设计哪里不同?
  9. 网站建设前后需要注意什么事项
  10. 为什么做好网站设计特别重要

最新案例