注:
因水平有限,不免有 不许确或过期 以内容,点击每节标题自动跳转至原文该节位置,强烈建议阅读官方文档!!!
二流运维,三流英语,译于2017年6月,发于2019年2月,偶尔更新。感谢阅读,欢迎反馈~
Docker 能够从 Dockerfile
中读取指令自动构建镜像,Dockerfile
是一个包含构建指定镜像全部命令的文本文件。Docker坚持使用特定的格式而且使用特定的命令。你能够在 Dockerfile参考 页面学习基本知识。若是你刚接触Dockerfile 你应该从哪里开始学习。php
这个文档囊括了Docker
公司和Docker
社区推荐的建立易于使用且实用的Dockerfile
的最佳实践和方法。咱们强烈建议你遵循这些规范(事实上,若是你建立一个官方镜像,你必须坚持这些实践。)python
你能够从 buildpack-deps Dockerifle看到许多这种实践和建议。nginx
注:本文档提到的Dockerfile命令的更详细的解释见 Dockerfile参考 页面。
从你的Dockerfile定义的镜像启动的容器应该尽量短暂。这里的『短暂』咱们是说它能够被中止和销毁而且一个新容器的构建和替换能够绝对最小化的变动和配置下完成。你可能想看下 应用方法论的12个事实中进程 一节来了解以无状态方式运行容器的动机。git
.dockerignore
文件在大多数状况下,最好把Dockerfile
放在一个空目录里。而后,只把构建Dockerfile
须要的文件追加到该目录中。为了改进构建性能,你也能够增长一个.dockerignore
文件来排除文件和目录。该文件支持与 .gitignore
相似的排除模式。更多建立.dockerignore
信息,见 .dockerignoregithub
为了减小复杂性,依赖,文件大小,和构建时间,你应该避免仅仅由于他们很好用而安装一些额外或者没必要要的包。例如,你不须要在一个数据库镜像中包含一个文本编辑器。golang
解耦应用为多个容器使水平扩容和复用容器更容易。例如,一个web应用栈会包含3个独立的容器,每一个都有本身独立的镜像,以解耦的方式来管理web应用,数据库。web
你可能据说过"一个容器一个进程"。这种说法有很好的意图,一个容器应该有一个操做系统进程并不是真的必要。除此以外,事实上如今容器能够 被init进程启动, 一些程序可能会本身产生其余额外的进程。例如,Celery 能够产生多个工做进程,或者 Apache 可能为每一个请求建立一个进程。固然"一个容器一个进程"一般是一个很好的经验法则,??但它不是一个很难和快速的规则(it is not a hard and fast rule)?? 用你最好的判断来保持容器尽量的干净和模块化。sql
若是容器之间相关依赖,你可使用 Docker容器网络 来取吧哦容器之间能够通讯。docker
你须要在Dockerfile可读性(从而能够长时间维护)和它用的层数最小化之间找到平衡。Be strategic 关注你使用的层数(and cautious about the number of layers you use).shell
不管什么时候,以排序多行参数来缓解之后的变化(Whenever possible, ease later changes by sorting multi-line arguments alphanumerically. )。这将帮助你避免重复的包而且使里列表更容易更新。这也使得PR更容易阅读和审查。在反斜线()前加一个空格也颇有帮助。
这里有个来自 buildpack-deps 镜像的实例:
RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
在构建镜像的过程当中,Docker会逐句读取你Dockerfile中的指令按指定的顺序执行。由于每一个指令都会被检查Docker会在它的缓存中查找能够重用的现有镜像(As each instruction is examined Docker will look for an existing image in its cache that it can reuse),而不是建立一个新的(重复的)镜像。若是你根本不像使用缓存,你能够对 docker build
命令使用 --no-cache=ture
参数。
然而,若是你使Docker使用缓存,那么理解它何时找到一个匹配的镜像以及什么不找就很是重要了。Docker将遵循的基本规则以下:
ADD
和COPY
指令,镜像中的文件内容被检查而且为每一个文件计算校验和。这些文件的最终修改和访问时间将不被考虑到校验和内。在查找缓存期间,校验和将被用于与已存在的镜像校验和进行对比。若是文件中有任何变化,好比内容或者元数据,那么缓存失效。ADD
和COPY
命令之外,缓存检查将不会检查容器中的文件来肯定缓存匹配。好比,当处理一个RUN apt-get -y update
容器中的文件更新将不会被检查来肯定是否命中已存在缓存。在这种状况下只有命令字符串本身将被用来查找匹配。一旦缓存失效,全部的后面的Dockerfile命令将会生成新的镜像并且不会使用缓存。
下面你会找到写Dockerfile里可用的各类指令的建议以及最佳方法。
不管什么时候只要可能使用当前官方仓库镜像做为你的基础镜像。咱们推荐Debian镜像, 由于它被严格控制而且保持最小(目前小于5MB),同时是一个完整的发行版。
你能够给你的镜像增长标签(labels)来协助经过项目组织镜像,记录受权信息,帮助自动化,或者其余缘由。每个标签都以LABEL
开头而且跟着一对或多对键值对。如下实例展现了可接受的不一样格式。解释性意见也包括在内(Explanatory comments are included inline.)。
注:若是你的字符串包含空格,它必须被引号引发来或者空格必须被转义。若是你的字符串包含内部引号字符("),他们须要转义。
# Set one or more individual labels LABEL com.example.version="0.0.1-beta" LABEL vendor="ACME Incorporated" LABEL com.example.release-date="2015-02-12" LABEL com.example.version.is-production="" # Set multiple labels on one line LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12" # Set multiple labels at once, using line-continuation characters to break long lines LABEL vendor=ACME\ Incorporated \ com.example.is-beta= \ com.example.is-production="" \ com.example.version="0.0.1-beta" \ com.example.release-date="2015-02-12"
查看 理解labels对象 获取可接受的标签键和值指导。
For information about querying labels, refer to the items related to filtering in Managing labels on objects.
跟以前同样,为了让你的Dockerfile
具备更高的可读性,更易于理解和维护,使用反斜线()将较长的或者复杂的RUN语句拆分为多行。
可能RUN
最多见的使用场景就是apt-get
的应用程序了。RUN apt-get
命令,由于使用它安装软件包有几个须要注意的问题。
你应该避免使用RUN apt-get upgrade
或者dis-upgrade
, 由于父镜像中许多"基本的"(essential)包不能在容器中升级。若是父镜像中有个软件包过时了,你应该联系它的维护者。若是你知道有个特定的软件包,foo
,须要升级,使用apt-get install -y foo
来自动升级。
一般把RUN apt-get update
和 apt-get install
合并到一个相同的RUN
语句中,例如:
RUN apt-get update && apt-get install -y \ package-bar \ package-baz \ package-foo
在一个RUN
语句中单独试用apt-get update
会引发缓存问题而且致使后面的apt-get install
指令执行失败。例如,你如今有个Dockerfile
:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl
镜像构建完成之后,全部的层都在Docker缓存中。假设你后来修改apt-get install
增长了其余的软件包:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl nginx
Docker将最初的指令和修改后的指令视为相同的指令(指apt-get update这行)而且使用上一步的缓存。结果就是apt-get update
没有执行由于使用了缓存的版本进行构建。由于apt-get update
没有执行,你的构建可能会安装一个过期版本的curl
和ngin
。
使用RUN apt-get update && apt-get install -y
能够确保你的Dockerfile
安装最新版本的软件包而无需编码或手动干预。这个技巧被称为"缓存破解"。你也能够经过指定软件包版原本破解缓存。这被称为固定版本,例如:
RUN apt-get update && apt-get install -y \ package-bar \ package-baz \ package-foo=1.3.*
固定版本在构建时强制查找指定版本的软件包而无论缓存有什么。这个技巧能够减小由于依赖包的未知变动致使的失败。
下面是一个格式规范的RUN
指令,实践了apt-get
的全部建议。
RUN apt-get update && apt-get install -y \ aufs-tools \ automake \ build-essential \ curl \ dpkg-sig \ libcap-dev \ libsqlite3-dev \ mercurial \ reprepro \ ruby1.9.1 \ ruby1.9.1-dev \ s3cmd=1.1.* \ && rm -rf /var/lib/apt/lists/*
s3cmd
指令指定了版本1.1.*
。 若是前一个镜像使用了一个老版本,指定新版本会引发apt-get update
的缓存破解以确保安装新版本。每行列出一个软件包能够避免包重复错误。
另外,你能够经过删除 /var/lib/apt/lists
清理apt缓存来减少镜像大小,由于apt缓存不会保存在层里。因为RUN语句以apt-get update
开头,因此在缓存apt-get
以前,包缓存将始终被刷新。
注:Debian和Ubuntu的镜像自动运行
apt-get clean
,因此不须要显式调用。
一些RUN
命令依赖使用管道符号(|)把一个命令的输出到另一个命令的能力,好比如下实例:
RUN wget -O - https://some.site | wc -l > /number
Docker试用/bin/sh -c
解释器执行这些命令,它只计算管道最后一个操做的退出代码来肯定是否成功。在上面这个例子中只要wc -l
命令执行成功这一步就构建成功而且生成一个新的镜像,即便wget
命令失败也是如此。
若是你想让管道中出现任意错误命令都返回错误,在命令前加上set -o pipefail &&
来确保避免出现未知错误时镜像也能构建成功。例如:
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
注:并不是全部的shell都支持-o pipefaile
选项。在这种状况下(好比dash
shell, 它是基于Debian镜像的默认shell),考虑使用RUN
的exec形式来显式选择一个支持pipefail选项的shell。例如:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD
指令用于运行你镜像包含中的软件,连同任意参数。CMD
应该尽量都是用这种形式 CMD [“executable”, “param1”, “param2”…]
。然而,若是是一个做为服务的镜像,好比Apache和Rails,你应该像这样执行CMD ["apache2","-DFOREGROUND"]
。实际上,实际上,这种形式的指令是推荐用于任何基于服务的镜像。
在其余大多数状况下,CMD
应该给一个交互式Shell,好比bash,python 和 perl。例如,CMD ["perl", "-de0"]
, CMD ["python"]
, 或者 CMD [“php”, “-a”]
。试用这种形式就意味着当你执行相似docker run -it python
的一些东西,你将获得一个可用的shell(you’ll get dropped into a usable shell, ready to go)。CMD
应该不多以CMD [“param”, “param”]
的形式和 ENTRYPOINT
一块儿试用,除非你和你的目标用户已经很是熟悉ENTRYPOINT
工做原理。
EXPOSE
指令指示容器将监听连接的端口。所以,你应该为你的应用程序试用通用的传统的端口。例如,一个包含Apache Web服务器的镜像应该EXPOSE 80
, 而一个包含MongoDB的镜像应该使用EXPOSE 27017
等。
对于外部访问,您的用户可使用指示如何将指定端口映射到所选端口的标志来执行docker run
。
???For container linking, Docker provides environment variables for the path from the recipient container back to the source (ie, MYSQL_PORT_3306_TCP).???
为了让软件更便于运行,你可使用ENV
来修改环境变量将软件安装目录加到PATH
。例如:ENV PATH /usr/local/nginx/bin:$PATH
将使 CMD [“nginx”]
能够工做。
ENV
指令也可用于给要容器化的服务所需的环境变量,好比Postgre的PGDATA
。
最后,ENV
也可用于指定通用版本号,这样版本易于维护,以下实例所示:
ENV PG_MAJOR 9.3 ENV PG_VERSION 9.3.4 RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && … ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
和程序中的常量变量相似(和硬编码值相反),这种方法让你能够修改一个单独的ENV
指令在容器中自动更新容器中的软件版本。
Dockerfile参考之 ADD指令
Dockerfile参考之 COPY指令
尽管ADD
和COPY
指令功能类似,通常而言,最好使用COPY
。是由于它比ADD
更透明。COPY
只支持最基本的从本地复制文件到容器中,而ADD
有更多功能(好比本地tar解压和远程URL支持)并非即刻课件的。所以,用ADD
最好的方式是本地tar文件自动提取到镜像,好比:ADD rootfs.tar.xz /
。
若是你有多个Dockerfile
步骤在你的上下文使用不一样的文件,单独COPY
他们,而不是一次复制全部。这将确保每一步的构建缓存(强制这一步从新运行)只有当它特定的依赖文件变化时失效。
例如:
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY . /tmp/
结论就是若是把COPY . /tmp/
放在RUN
以前失效缓存更少。
由于镜像大小很重要,使用ADD
来获取远程URLs是强烈反对的;你应该使用curl
和wget
替代。这种方式你能够在解压后不须要时删除这些文件而且你不会在你的镜像增长额外一层。例如,你应该避免这么作:
ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all
而且以此种方式替代:
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz \ | tar -xJC /usr/src/things \ && make -C /usr/src/things all
对于不须要ADD tar自动提取功能的其余项目(文件,目录),应始终使用COPY。
使用ENTRYPOINT
最好的方式是设置镜像主命令,容许镜像把它做为命令运行(而后使用CMD
做为默认标识)。
咱们从一个命令行工具s3cmd
镜像的例子开始:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
如今能够像这样运行镜像来显示命令的帮助:
$ docker run s3cmd
或者使用正确的参数来执行一个命令:
$ docker run s3cmd ls s3://mybucket
这样有用,由于镜像名称能够复用为二进制文件的引用,如上面命令所示。
ENTRYPOINT指令也能够与辅助脚本组合使用,容许其以相似于上述命令的方式运行,即便启动工具可能须要多于一个步骤。
例如,Postgres官方镜像 使用如下脚本做为它的 ENTRYPOINT
:
#!/bin/bash set -e if [ "$1" = 'postgres' ]; then chown -R postgres "$PGDATA" if [ -z "$(ls -A "$PGDATA")" ]; then gosu postgres initdb fi exec gosu postgres "$@" fi exec "$@"
注:这个脚本使用
exec
Bash命令 以便最终运行的应用程序成为容器PID 1。这样作容许应用程序接受发送给容器的Unix信号。查看
ENTRYPOINT帮助获取更多细节。
帮助脚本被拷贝到容器而且当容器启动时经过ENTRYPOINT
运行。
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
此脚本容许用户以多种方式与Postgres进行交互。
它能够简单的启动Postgres:
$ docker run postgres
或者,能够运行Postgres而且传递参数给服务器:
$ docker run postgres postgres --help
最后,它也能够被用于启动一个彻底不一样的工具,好比Bash:
$ docker run --rm -it postgres bash
VOLUME
指令应该用于暴露任意数据库存储区,配置存储,或者docker容器建立的文件/目录等。强烈建议您将VOLUME用于镜像的任何可变和/或用户可维护的部分。
若是服务能够没有权限运行,使用USER
变为一个非root用户。像以下命令同样开始在Dockerfile
中建立用户和组:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
注: ??? (Users and groups in an image get a non-deterministic UID/GID in that the “next” UID/GID gets assigned regardless of image rebuilds. )???因此,若是很重要的话,你须要显式指定UID/GID。注:因为Go存档/ tar包处理稀疏文件中的一个未解决的bug, 在docker容器里建立一个UID足够大的用户会在容器层中将
/var/log/faillog
写满NUL (\0)
而致使磁盘耗尽。传--no-log--init
标记来建立用户能够绕开这个问题。Debian/Ubuntu的adduser
包不支持--no-log-init
标记因此应该避免使用。
你应该避免安装和使用sudo
,由于它不可预知的TTY和信号转发行为带来的问题比解决的问题多。若是你确实须要相似sudo
的功能(例如:以root用户初始化可是以非root用户运行),你可使用"gosu"。
最后,减小你的层和复杂性,避免切换用户(Lastly, to reduce layers and complexity, avoid switching USER back and forth frequently.)。
为了清晰可靠,你应该在使用WORDDIR
时应该一直使用绝对路径。你也应该使用WORKDIR
而不是使用像RUN cd .. && do-something
这样难以阅读、调错和维护的增量指令。
ONBUILD
命令在当前Dockerfile
构建完成以后执行。ONBUILD
会在任意一个从当前镜像派生的子镜像执行。能够把ONBUOLD
命令想象成为一个父级Dockerfile
赋予子Dockerfile
的指令。
Docker构建在子Dockerfile
中的任何命令以前执行ONBUILD
命令。
ONBUILD is useful for images that are going to be built FROM a given image. For example, you would use ONBUILD for a language stack image that builds arbitrary user software written in that language within the Dockerfile, as you can see in Ruby’s ONBUILD variants.
Images built from ONBUILD should get a separate tag, for example: ruby:1.9-onbuild or ruby:2.0-onbuild.
当在ONBUILD
中使用ADD
或者COPY
时要当心。若是新构建的上下文丢失了增长的资源,"onbuild"的镜像将会严重失败。如上所述,添加单独的标签,容许Dockerfile
的做者本身选择有助于缓解这种状况。
这些官方仓库有典型的示范(These Official Repositories have exemplary Dockerfiles):
Alpine
apk cache clean rm -rf /var/cache/apk/* ~/.cache/* /usr/local/share/man
Debian/Ubuntu
apt-get autoremove rm -rf /var/lib/apt/lists/* ~/.cache/* /usr/local/share/man
RedHat/CentOS
yum clean all rm -rf /var/cache/yum/* ~/.cache/* /usr/local/share/man
不少镜像默认使用UTC时间,可是面向中国用户的的大多应用,在获取系统时间时直接取系统时间并不会作一个校对,这个时候就会出现程序获取的时间或者日志时间和实际不一致的状况。
分享个例子,曾接到研发同事反馈容器内时间不对致使的小问题,因而着手修复。完成后开始检查其余生产环境中容器时间和时区,发现生产环境中有多大31个应用时区不对(某些应用是同一个镜像仓库,大约涉及20多个镜像,及十多个代码仓库),虽然其余应用暂时没有致使严重的问题,可是必然是个隐患,因而开始着手修复,修改Dockerfile
-> 提交 -> 构建 -> 部署,老实说,这是纯体力活。。。。
因此一开始就应该作这件事。
针对全球环境而言,保证时间一致性仍是建议统一使用UTC时间,包括但不限于内容:
MySQL
,PostgreSQL
;若是出现数据、系统日志、应用日志没法对齐的状况,有不少场景会让人焦头烂额,如:数据整合、统计、日志分析等等。
前提宿主机时区正确,详情参考:设置时区(v0.1)
不建议修改容器及镜像内时区,若须要保证容器时区与宿主机保持一致,经过如下参数将宿主机时区文件挂载到容器便可。
# docker run docker run -v /etc/localtime:/etc/localtime:ro xxxx # docker-compose ... volume: - /etc/localtime:/etc/localtime:ro ...
Alpine
修改时区
apk update && add tzdata ca-certificates ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
Debian/Ubuntu
修改时区
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime dpkg-reconfigure -f noninteractive tzdata
Centos/RedHat
修改时区
# CentOS的时区配置文件是:/etc/sysconfig/clock ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # CentOS/ RHEL 7 Only timedatectl set-timezone /etc/localtime
CentOS的时区配置文件是:/etc/sysconfig/clock
,配置文件有以下几个配置选项:
BIOS
中保存的时间是不是GMT/UTC
时间,true
表示BIOS
里面保存的时间是UTC
时间,false
表示BIOS
里面保存的时间是本地时间ZONE的
值是一个文件的相对路径名,这个文件是相对 /usr/share/zoneinfo
目录下的一个时区文件。好比ZONE
的值能够是:Asia/Shanghai
, US/Pacific
, UTC
等false
,在一些特殊硬件(Alpha
)下才配置该选项为true
false
,在一下特殊硬件下才配置该选项为false
/etc/sysconfig/clock
的配置实例
ZONE="Asia/Shanghai" UTC=true ARC=false
说明:这个配置文件里面的参数和hwclock
命令关系很大,系统在启动的时候读取/etc/sysconfig/clock
文件的内容,根据这些内容调用hwclock
命令