关于Dockerfile的7个建议和技巧

上个月看到 Docker Global Mentor Week 2016的庆祝会,一个伟大的计划就是由Docker帮助用户改善所有技术水平。Docker是resin.io堆栈中一个关键技术,我们已经发现有很多与Docker有关的最佳实践、提示和技巧,可以显著的改善resin.io开发人员的体验。Docker已经有一个最佳实践集合,但是不是它们所有都适用resin.io用例。本着Global Mentor Week的精神,在此博文中,我收集了我们因resin.io应用程序和硬件设备的最高影响的Docker建议。

下面的笔记分为2个主要部分:缺一不可的实践,真正的每时每刻你都会用到,锦上添花的建议可以进一步的改善你的代码和体验,但有点严格。

缺一不可

接下来的实践也许会减少你在开发过程中很多痛苦。

添加软件版本

在最佳实践阵容里,赢家的是添加所有软件的相关版本。包括基础镜像,从Github上带走的代码,代码依赖的类库等等。通过版本控制,可以更简单的约束一个已知的工作发布应用程序。没有它,组件很容易被改,这样一个以前的工作Dockerfile不再创建了。

你可以在 resin.io Docker Hub listing上找到最新的、可用的标记数据基础镜像版本,选择基础镜像并且看Tags 标签。例如,这里有个tag列表resin/raspberrypi3-debian。因此需要举例使用jessie-20161119,替代简单的 jessie tag,后者之后的变化:

FROM resin/raspberrypi3-debian:jessie-20161119

我们的基础镜像的结构有时候会变(很少,但是有),当使用日期tag,可以依赖一个已知的、好的基础镜像版本(由Docker提供,永远可供下载)。

一个更加棘手的事情就是从操作系统的软件包管理器安装添加版本号的软件。在Debian里,通过特定的版本信息将运行apt-get ,例如:

RUN apt-get update && \
 apt-get install -yq --no-install-recommends \
 i2c-tools=3.1.1-1 \
 ...

一些支持 Debian packages, Alpine packages, 和 Fedora packages,和它们各自的软件包管理器。如果你已经安装了一个不错的程序包,它需要多一点的详细调查来设置添加版本号,长远来看是值得的。

很多时候,将从版本控制中安装软件(例如从git/Github),在这种情况下,没有理由不使用特定提交,由一个唯一的ID定义(例如适合于git的hash/SHA),或者一个tag。这有个例子,如何用git检验一个特定标记版本代码:

# Can use tag or commit hash to set MRAAVERSION
 ENV MRAAVERSION v1.3.0
 RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
 cd mraa && \
 git checkout -b build ${MRAAVERSION} && \
 ...

最后,添加的内容需要应用于你安装的每个类库,不论使用requirements.txt (Python), package.json (Node.js), Cargo.toml (Rust),或一些其他的编程语言的软件管理包。总是把外部类库添加(或者常被称为锁定或冻结)一个版本号或唯一的提交。

不留痕迹

常见的知识,提升电脑程序速度最好的方法之一就是剔除不必要的运算(“让它少做点”)。这知识同样支持软件部署:提升部署和升级的最好的方法是不去发布不需要的代码。在我们的例子中,不留痕迹的清理并从容器中删除不需要的bits。

什么是不需要的bits?最普通的就是软件包管理器留下的临时文件或者在Dockerfile里创建和安装的软件的源代码。

软件包管理器后的清理方法取决于在基础镜像里的分布。在Debian和Raspbian的情况下,就是apt-get, 在Dockerfile中,Docker已经有相当多的建议关于使用 apt-get 。这意味着安装完成并除去临时信息,例如:

RUN apt-get update && \
 apt-get install -yq --no-install-recommends \
  \
 && apt-get clean && rm -rf /var/lib/apt/lists/*

上面的最后一行,通过 apt-get 除去遗留的临时文件,在你的设备上你将不需要。

如果使用Apline Linux, apk 程序包管理工具有一个方便的 –no-cache 选项,可以不留下任何东西:

RUN apk add --no-cache

对于Fedora, dnf 程序包管理可以类似的处理apt-get:

RUN dnf makecache && \
 dnf install -y \
  \
 && dnf clean all && rm -rf /var/cache/dnf/*

清理安装软件的原代码通常很简单,删除创建项目中之前步骤里所创建的目录。还是上面的MRAA例子,这是git检验后的一个清理方法:

ENV MRAAVERSION v1.3.0
 RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
 cd mraa && \
 git checkout -b build ${MRAAVERSION} && \
 
 make install && \
 cd .. && rm -rf mraa

另外确保让所有的清理语句在同样的 RUN 章节,否则就会好像是清理了,但是仍然像压舱底一样存在于最后的Docker容器中。

结合RUN语句

上面最后一个注意联系到“缺一不可”最后一个实践,在Dockerfile里符合逻辑的结合RUN语句。符合逻辑的合成整体的步骤应该在同样的语句中,为了避免几个常见问题,通常与缓存和使用不必要的磁盘空间有关。首先,由于缓存会有意想不到的创建产出。如果 apt-get update 步骤在单独的RUN里,而它来自 apt-get install 步骤,前者会缓存并且当你期望它升级的时候却不升级。如果分开git clone 并且实际创建,类似的事情可能发生。

第二,在最后的容器中,在之后单独的RUN步骤中文件删除是保留的,但是不容易找到(压舱底)。

在这个建议上,Docker文件有更多一些的注意和背景。

锦上添花

强烈推荐接下来的实践,通常让你的体验更上一层,但是为了把事做好没必要变成一个阻碍。

Dockerfile语句顺序

Docker试图在Dockerfile中缓存所有步骤且不改变,但是如果改变一些语句,接下来所有的步骤将会重做。通过安排Dockerfile按照最小的次序排序可能更容易改变,可以在创建项目中节省不少时间,只要可能。例如,一般的设置比如设置工作目录,授权系统初始化,设置维护人员。

MAINTAINER Awesome Developer <awesome@developer.net>
 WORKDIR /usr/src/app
 ENV INITSYSTEM on

这些语句可以遵循使用操作系统程序包管理器的安装包,随后编辑相关性,授权系统服务和其他设置。例如,对Dockerfile这一节结束的部分,可以安装Python:

COPY requirements.txt ./
 RUN pip install -r requirements.txt

或者Node.js 相关。

COPY package.json ./
 RUN npm install

复制应用程序源代码应该接近尾声了,因为很多时候这是最容易改变的。它就是一个“复制所有”的命令,就像:

COPY . ./

这个方法可以加速创建和部署项目,Dockerfile将也会更易读。上面的例子仅供参考,逻辑顺序极大的取决于你独有的应用程序。

使用.dockerignore

连着之前的步骤,总是定义一个 .dockerignore,来告诉工程师源代码的内容将不需要在设备本身上运作,不是在 COPY . ./ 步骤复制。README.md 或其他文件容易被忽视,文件所包含的图像或者其他部分,是应用程序功能不需要的,但是为了一些原因把它们保存在同样的资源库中。

使用一个启动脚本

在创建(和调试)大量的项目,这是个人建议:别从 CMD 步骤调用应用程序,但从这调用启动脚本:

CMD ["bash", "start.sh"]

然后,在start.sh里,你可以有,例如 python app.py 或其他方法来启动你的应用程序。优势是比不断重写 CMD 更容易扩展和增加调试来启动脚本。你想在主代码开始前,发出一些调试信息吗?给你的启动脚本增加同样多的行和同样多的测试逻辑。

另一方面,你也可以给部署和使用 resin sync的测试来提速。Resin sync可以复制应用程序源代码同步到一个运行中的设备并适当升级(不需要重建Dockerfile),然后通过升级设置重启容器。无论如何,如果Docker没有缓存该文件,它只能有效,例如在 CMD 中直接被引用。

创建一个Non-Root用户

通过Docker默认,在应用程序容器中的代码通过root运行。作为一个良好的安全实践,建议创建一个non-root用户,并且根据需要尽可能多授权给它合适的权限。例如:

RUN useradd --user-group --shell /bin/false resin
 USER resin

这将会创建一个名叫resin的用户,并且作为那个用户,运行所有的后来的步骤。在Docker docs, 或 this blog post(https://nodesource.com/blog/8-protips-to-start-killing-it-when-dockerizing-node-js/#protip1createanonrootuser)查看更多。

总结

为了进一步研究,检查我们在 Build Optimization 或者Docker的 Best practices for writing Dockerfiles(那些应用)的文件。你也许也想看一看 Dockerfile Linter 上的普通改进和建议。

你有没有一些其他在resin.io 上的Docker最佳实践并愿意分享吗?在评论里留下你的建议,在Gitter上联系我们,或者顺便逛逛论坛!愿闻其详!

分享到:更多 ()