九个编写Dockerfiles的常见错误

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

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的常见错误
相关文章
|
存储 算法 Java
【算法系列篇】滑动窗口-1
【算法系列篇】滑动窗口-1
|
3月前
|
人工智能 自然语言处理 数据挖掘
OPC一人公司和个体户的区别:这不是换了个马甲这么简单
2026年浙江发布OPC团体标准,明确OPC(一人公司)非个体户升级版:它是由1名核心自然人主导、以AI为生产力主体、具备法人资格、拥有可复制业务系统的轻量化技术公司。核心差异在于有限责任保护、AI驱动规模化生产及系统化收入模型。(239字)
|
3月前
|
人工智能 自然语言处理 数据可视化
企业如何监测品牌在豆包/AI回答中的出现频率
当用户转向豆包、Kimi等AI直接提问,你的品牌是否仍被提及?本文提供轻量、可落地的AI品牌监测方案:覆盖问题库构建、API自动化采集、文本分析与可视化预警,助企业抢占AI时代用户心智高地。
432 2
|
4月前
|
人工智能 运维 安全
Hermes Agent 核心必学:SubAgent 子代理的 5 个实战技巧,多任务处理效率翻倍
Hermes Agent SubAgent子代理完整教程:掌握delegate_task并行委派、上下文隔离与多任务处理核心能力,提升开发效率。
1126 1
|
4月前
|
数据采集 Web App开发 JavaScript
房源信息采集:链家/贝壳等房产网站的反爬策略应对方案
本文详解链家/贝壳房产数据采集的反爬困境与实战方案:针对IP封禁、滑块验证、JS动态渲染及“幽灵房”假数据等难题,提出OpenClaw驱动真实浏览器+站大爷高匿隧道代理+请求频率与指纹伪装三重防护策略,兼顾稳定性与合规性。(239字)
734 0
|
11月前
|
存储 网络协议 测试技术
阿里云通用算力型实例u1/u2i/u2a对比,差异解析与选型参考
目前通用算力型实例规格已经有u1、u2i、u2a这三大实例类型,有的新手用户便宜不是很清楚他们之间有何区别?我们应该如何选择?本文从技术架构、性能指标、适用场景到收费价格,对比三大实例,以供了解和选择参考。
|
6月前
|
弹性计算 人工智能 关系型数据库
最新阿里云服务器租用价格:轻量应用服务器抢购、云服务器特惠、u2i及九代实例活动价格
2026年阿里云服务器租用价格优惠力度大,涵盖多个规格与场景。轻量应用服务器性价比极高,2核2G配置新用户抢购价低至38元/年,2核4G配置月付9.9元起、年付199元起。ECS云服务器低价长效特惠,经济型e实例2核2G特惠价99元/年,通用算力型u1实例2核4G新购续费均为199元/年。此外,u2i实例提供2核到4核多种配置,3折起优惠;第九代企业级实例(c9i/g9i/r9i)享6.4折起优惠。
|
IDE 开发工具 数据安全/隐私保护
Python循环嵌套:从入门到实战的完整指南
循环嵌套是Python中处理多维数据和复杂逻辑的重要工具。本文通过实例讲解嵌套循环的基本用法、常见组合、性能优化技巧及实战应用,帮助开发者掌握其核心思想,避免常见错误,并探索替代方案与进阶方向。
890 0
|
XML Java Android开发
Android 动画之帧动画 + 补间动画 + 属性动画
本文介绍了Android开发中的三种动画类型:帧动画、补间动画和属性动画。帧动画通过依次播放一系列静态图片实现动态效果,支持Java代码与XML两种实现方式。补间动画基于起始和结束位置自动生成过渡效果,涵盖透明度、位移、旋转、缩放及组合动画等多种形式,并可搭配插值器优化动画过程。属性动画则通过改变对象属性实现动画,支持透明度、位移、旋转、缩放及组合动画,灵活性更高且适用于更复杂的场景。文中提供了详细的代码示例,帮助开发者快速上手。
685 15

热门文章

最新文章