【编者的话】本文主要讲述如何优化Dockerfile,来缩短docker镜像构建需要的时间,以及Dockerfile的一些编辑规范,推荐所有的Docker爱好者阅读,非常基础的文章,本文也许会给你一些启发和指导。
Docker镜像应该是小而快的。然而,假设你在BusyBox镜像中预编译GO二进制文件,他们就会变得又大又复杂。如果不能构建一个良好的Dockerfile来帮助你提高构建缓存命中率,那么你的镜像构建过程将会变得相当的缓慢。
比如一个用于软件安装的bash脚本,里面堆砌着大量的curl、wget等命令语句,大家在写Dockerfile的时候通常就会像写这个bash脚本一样,将一系列的Docker命令堆砌在其中,这种Dockerfile在构建镜像的时候是比较低效和缓慢的。
当你正在为一个应用程序构建一个新的Dockerfile,在决定需要引哪些包、运行什么命令的时候肯定会进行很多次尝试,也会遇到很多的问题。优化你的Dockerfile确保命中“构建缓存”的概率越来越大,这样之后的每一次构建中会比前一次要更快一些。
一般的规律是有频率的改变Dockerfile中命令的排序,观察分析运行命令所耗费的时间及与其他镜像共享资源的方式。
这就意味着像WORKDIR、CMD、ENV这些命令应该在底部,然而一个 RUN apt-get -y update
更新应该在上面,因为它需要更长时间来运行,也可以与你所有的镜像共享。
最后任何ADD(或其它缓存失效的命令)命令应该尽可能地在Dockerfile底部,在那里你有可能做出很多改变,然后后续命令缓存失效。
在如Ubuntu这样的操作系统镜像和Python或Java7中一个特定的应用程序中,有很多基础镜像可供选择。常识告诉你使用Ruby2来运行基于Ruby应用程序并且使用Python3运行Python应用程序。但是现在你有两个几乎没有共同之处的基础镜像,所以你需要下载和构建。相反,如果你使用Ubuntu运行这两个程序,你只需要下载一次基础镜像。
在一个Dockerfile中每个命令都会在原来的基础上生成一层镜像。你可以很快的在三十多层的时候就结束了,这未必是一个问题,但也可以通过组合RUN命令,并使用一行EXPOSE命令列出你所有的开放端口,这样可以有效减少镜像的层数。
通过将RUN命令分组,可以在容器间分享更多的层。当然如果你有一组命令可以多个容器通用,那么你应该创建一个独立的基础镜像,它包含你建立的所有镜像。
对于每一层来说你都可以跨多个镜像分享,这样可以节省大量的磁盘空间。
在创建容器并考虑到体积问题的时候,不要为了节省空间去使用体积小的镜像,尽量使用你将要提供数据的应用程序打成的镜像。如果你这样做了,并且提交了磁盘数据,你不仅在容器中储存了你的数据,而且对实际应用程序的调试也非常有用。
当你已经构建了一个镜像,在运行它的时候发现有一个package缺少了,把它添加到Dockerfile的底部,而不是添加到顶部的run apt-get命令那里。这意味着你能尽快的重新构建这个镜像了。一旦你的镜像可以正常工作,你可以再提交源码之前重新优化整理Dockerfile。
如果一个Dockerfile是由类似于一个bash脚本写出来的,那么它可能会是这样的:
FROM ubuntu:trusty
MAINTAINER Paul Czarkowski "paul@paulcz.net"
RUN apt-get -yq update
# Apache
RUN /
apt-get -yqq install /
apache2 /
apache2-utils /
libapache2-mod-python /
python-dev /
python-pip /
python-cairo /
python-pysqlite2 /
python-mysqldb /
python-jinja2
sqlite3 /
curl /
wget /
git /
software-properties-common
RUN /
curl -sSL https://bootstrap.pypa.io/get-pip.py | python && /
pip install whisper /
carbon /
graphite-web /
'Twisted<12.0' /
'django<1.6' /
django-tagging
# Add start scripts etc
ADD . /app
RUN mkdir -p /app/wsgi
RUN useradd -d /app -c 'application' -s '/bin/false' graphite
RUN chmod +x /app/bin/*
RUN chown -R graphite:graphite /app
RUN chown -R graphite:graphite /opt/graphite
RUN rm -f /etc/apache2/sites-enabled/*
ADD ./apache-graphite.conf /etc/apache2/sites-enabled/apache-graphite.conf
# Expose ports.
EXPOSE 80
EXPOSE 2003
EXPOSE 2004
EXPOSE 7002
ENV APACHE_CONFDIR /etc/apache2
ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_PID_FILE $APACHE_RUN_DIR/apache2.pid
ENV APACHE_LOCK_DIR /var/lock/apache2
ENV APACHE_LOG_DIR /var/log/apache2
WORKDIR /app
# Define default command.
CMD ["/app/bin/start_graphite"]
然而这个Dockerfile的优化版本是基于之前所讨论的内容的,它看起来是这样的:
# 1 - Common Header / Packages
FROM ubuntu:trusty
MAINTAINER Paul Czarkowski "paul@paulcz.net"
RUN apt-get -yq update /
&& apt-get -yqq install /
wget /
curl /
git /
software-properties-common
# 2 - Python
RUN /
apt-get -yqq install /
python-dev /
python-pip /
python-pysqlite2 /
python-mysqldb
# 3 - Apache
RUN /
apt-get -yqq install /
apache2 /
apache2-utils
# 4 - Apache ENVs
ENV APACHE_CONFDIR /etc/apache2
ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_PID_FILE $APACHE_RUN_DIR/apache2.pid
ENV APACHE_LOCK_DIR /var/lock/apache2
ENV APACHE_LOG_DIR /var/log/apache2
# 5 - Graphite and Deps
RUN /
apt-get -yqq install /
libapache2-mod-python /
python-cairo /
python-jinja2 /
sqlite3
RUN /
pip install whisper /
carbon /
graphite-web /
'Twisted<12.0' /
'django<1.6' /
django-tagging
# 6 - Other
EXPOSE 80 2003 2004 7002
WORKDIR /app
VOLUME /opt/graphite/data
# Define default command.
CMD ["/app/bin/start_graphite"]
# 7 - First use of ADD
ADD . /app
# 8 - Final setup
RUN mkdir -p /app/wsgi /
&& useradd -d /app -c 'application' -s '/bin/false' graphite /
&& chmod +x /app/bin/* /
&& chown -R graphite:graphite /app /
&& chown -R graphite:graphite /opt/graphite /
&& rm -f /etc/apache2/sites-enabled/* /
&& mv /app/apache-graphite.conf /etc/apache2/sites-enabled/apache-graphite.conf
这是最常见的共享层,在同一个主机上运行所有镜像应该从它开始。你可以看到我已经添加了一些诸如curl和git的操作,他们不是必须的,但是对调试很有用。而且因为他们在分享层,所以它们不会占用太多空间。
现在说一下我们的语言规范。在这里我已经包含了python和apache的部分,因为到底把谁放在第一位并不十分清楚。如果我们把apache放在第一位,我们可以获得一个包含层和免费得到apache的ruby应用程序。
我把这些单独说出来是为了以下这些原因。
首先,镜像中添加Apache部分之后直接构建其他需要的部分模块,以便于在构建多个镜像的过程中尽量多应用公共缓存。你也许认为这并不重要,因为类似EVN的调用是很便宜的,但是我见到过随机的ENV调用耗费了10秒钟甚至更多时间。
有一个很好的例子:你可能想要在容器的底部启动,但这些命令不能被改变的,那么最好把他们移到略微靠前一点的地方。
其次,我真希望Docker能够在同一行指定多个环境,这样可以减少层数,最终提供了一种最简化的构建方式。
这包含了一些特定的apt和pip等资源包。你可以在一个单一的命令中加入他们,利用&&符号最为分隔符,如果需要修改只需要修改这条组合命令即可。
这包含了一大堆简易的命令,如ADD和VOLUME,比起以前安装的包,他们更不可能改变,但运行的效率也不慢,所以在这些命令的缓存失效以后就会变得并不那么重要了。
所以建议把类似的这些命令放在Dockerfile的底部。
你应该在最后面使用ADD命令。
将这些类似ADD命令的操作放入最后一层。
希望这篇文章能够帮你编写一个更好的Dockerfile文件。这些都是我在构建我自己的镜像的时候所经历过的,虽然他们可能并不适用于所有情况(或可能是错误的),但他们确实提高了我的开发经验。
原文链接: Optimizing your Dockerfiles (翻译:王康 翻译:李颖杰)
===========================
译者介绍
王康,现就职于sion,高级软件工程师,负责云图部署服务的相关工作。曾就职于momo。个人比较热爱新兴技术,热爱共享学习。