容器的OCI标准定义了容器镜像规范,容器镜像包与传统的压缩包(zip/tgz等)相比有两个关键区别点:1)分层存储;2)打包即部署。html
分层存储能够极大减小镜像更新时候拉取镜像包的时间,一般应用程序更新升级都只是更新业务层(如Java程序的jar包),而镜像中的操做系统Lib层、运行时(如Jre)层等文件不会频繁更新。所以新版本镜像实质有变化的只有很小的一部分,在更新升级时候也只会从镜像仓库拉取很小的文件,因此速度很快。java
打包即部署是指在容器镜像制做过程包含了传统软件包部署的过程(安装依赖的操做系统库或工具、建立用户、建立运行目录、解压、设置文件权限等等),这么作的好处是把应用及其依赖封装到了一个相对封闭的环境,减小了应用对外部环境的依赖,加强了应用在各类不一样环境下的行为一致性,同时也减小了应用部署时间。node
基于分层存储与打包即部署的特性,容器能够更快的部署、运行、保持环境一致性,这些特性都是容器相对于传统虚拟机打包部署的优点。这两个点都是经过dockerfile来承载的,所以如何编写高效、安全、规范易用的dockerfile是容器实践中关键的一个环节。
<!--more-->python
<iframe width='853' height='480' src='https://embed.coggle.it/diagr...' frameborder='0' allowfullscreen></iframe>nginx
不少基础镜像都提供不一样功能集的版本,根据实际状况选择最小功能集的版本,功能越少体积越小,引入安全漏洞的风险越低。
如Node镜像,最小的才不到30M,最大的超过200M。git
latest 是一个不肯定的版本号,依赖 latest 会致使每次构建出的镜像是不可预知的。github
依赖基础镜像版本时候,尽可能指定到最具体的版本,这样不肯定性最小。如Node镜像,版本号有 12, 12.4, 12.4.0,依赖 12.4.0 是不肯定性最小的。redis
容器镜像仓库中的tag是能够被复写的,同一个tag在不一样的时间对应内容可能不一样,所以若是条件容许,将依赖的镜像mirror一份到本地,这样能够保证
每次构建时候依赖的基础镜像是不变的。docker
Docker 1.10 版本开始,Docker官方镜像仓库与Docker-EE/Docker-CE都支持镜像多平台功能,这里的多平台包含不一样类型操做系统(Windows/Linux),不一样CPU体系(X86/ARM64)。缓存
利用镜像多平台功能,在docker pull ...
命令中能够不用显示指定平台类型,docker客户端会根据当前的平台架构与镜像仓库协商(基于manifest list特性),拉取对应平台的镜像。
经过这个特性能够保持用户界面的简洁性,客户端执行 docker pull node:12
命令,在ARM64机器上,则会拉取 arm64v8/node:12
镜像,在Linux机器上,则拉取 node:12
镜像。
可是这种方式也引入了不肯定性,不肯定性表现为2方面:第一方面为不一样平台上版本存在差别,可能依赖的版本只在一个平台有;第二个方面为有些时间可能在Linux环境下构建ARM镜像,这样会致使构建出错误镜像。
经过Label增长镜像做者,构建时间,描述等信息,让使用者获得更多关于镜像信息。
经过Label增长镜像对应应用的 securitytxt 信息。
securitytxt 是 IETF 组织起草的一份规范,目的定义一套互联网服务提供者与安全漏洞发现者之间交互的规范,使得安全漏洞可以被闭环。
RUN, COPY 命令都要使用到绝对路径,定义好 WORKDIR 会使得 Dockerfile 移植性更好,更容易维护。
ENV, ARG 的值都会被记录下来,经过 docker image history
命令能够查看到,所以不要将敏感信息传递给ENV与ARG。
若是须要在dockerfile中使用密钥或凭证,使用 mount secret 方式。
因为 docker build cache 机制,有上下文依赖的命令放到同一个RUN指令中执行。
假若有以下dockefile片断:
... RUN apt-get update RUN apt-get install -y nginx ...
过了一段时间以后,须要修改一下上述dockerfile,增长一个安装包
... RUN apt-get update RUN apt-get install -y nginx python ...
此时构建镜像,docker 比对缓存,RUN apt-get update
这一层已经存在,使用缓存。这时候apt仓库中的python或nginx可能已经有新版本了,因为没有执行 apt-get update
,所以安装的并非最新版本。
经过构建参数 build --no-cache
显示使得缓存失效,不过缓存失效会增长构建时长,须要综合考虑。
假如在dockerfile中执行命令 RUN wget -O - https://some.site | wc -l > /number
,若是 wget -O - https://some.site
执行失败了,可是 wc -l
是成功的,所以并不会报错退出。
使用 set -o pipefail
避免管道错误被忽略。上述命令修改成: RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
。
ADD 命令功能相对 COPY 更复杂,包含从internet下载,解压等,优先使用 COPY 。
不推荐使用 ADD 命令从远程下载一个软件包解压到镜像内,推荐使用 curl 下载到本地,而后再解压到镜像内,并将原始文件删除。
root用户运行应用程序存在安全风险,为每一个应用单首创建一个运行用户。
因为dockerfile中建立用户/用户组的 UID/GID 是不固定的,若是应用程序依赖 UID/GID,建立用户时候显示指定。
应用程序运行用户设置Shell为 /sbin/nologin 。
经过 EXPOSE 指令申明应用 listen 的端口与协议,让应用运维人员者简单明了得知端口。
EXPOSE 80/tcp
程序持久化或临时文件目录,使用 VOLUME 指令申明,让应用运维人员简单明了得知须要挂载的卷。
日志输出到标准输出与错误输出,方便查看与采集日志。参考 Nginx 的 dockerfile :
# forward request and error logs to docker log collector RUN ln -sf /dev/stdout /var/log/nginx/access.log \ && ln -sf /dev/stderr /var/log/nginx/error.log
推荐将容器内的应用程序设置为PID 1,这样对容器内应用管理相对简单,1号进程退出后整个容器也就退出了。
在dockerfile中使用 CMD 或 ENTRYPOINT 指令设置容器镜像的默认启动进程, CMD/ENTRYPOINT 有两种执行方式:exec 与 /bin/sh ,使用 exec 方式直接执行应用程序可执行文件设置PID为1,使用 /bin/sh 方式的话,1号进程就是 /bin/sh 。
exec 方式: CMD["java", "/same/args"]
, ENTRYPOINT ["java", "/same/args"]
, /bin/sh 方式: CMD java /same/args
, ENTRYPOINT java /same/args
。
ENTRYPOINT 与 CMD 功能相似,他们区别是 ENTRYPOINT 不能被 docker run 的命令行参数覆盖,而 CMD 能够; ENTRYPOINT 一般配合 CMD 使用,使用 CMD 设置 ENTRYPOINT 的默认参数,同时支持在 docker run 设置参数传递给 ENTRYPOINT。
参考Redis的ENTRYPOINT写法。
对于切换用户,推荐使用 gosu 替换 sudo 。
若是在运行应用程序以前须要作一些准备工做,如检查文件/目录权限等,那么能够在 ENTRYPOINT 的脚本中完成。
经过K8S调度docker镜像能够覆盖dockerfile中的 ENTRYPOINT 与 CMD,他们之间的关系参考以下文章。
1.12 使用hadolint工具检查dockerfile
hadolint 定义了一套规则与检查工具,能够在项目中使用。
参考 1.1.1 。
chmod -R /a/root/path
,更改具体某个/类文件的权限因为dockerfile构建镜像使用的是分层只读文件系统,若是使用 chmod 更改一个大目录权限,至关于复制了这个目录下全部文件,会致使镜像体积变大。
Docker 17.05 版本中引入了分阶段构建({Multi-stage builds](https://docs.docker.com/devel...),经过分阶段构建能够减小镜像大小。
Docker镜像的分层是子层依赖父层,对应到 Docker build cache 机制中,若是某一层未命中缓存,那么其剩下的层都不会使用缓存。
所以若是须要最大利用缓存机制,推荐将变化频度低的层尽可能放上层,变化频度高的层放下层。
apg-get upgrade 会使得镜像大小不可控,不知道更新了多少软件包。
一样是考虑缓存命中率,变化频度一致的命令放到同一层。
Docker在构建镜像时候有一个 build context 概念,build context 在 docker build 指定一个目录,docker 会将 build context 目录内全部文件加载到内存,做为build context。
build context 目录内内容越少,加载速度越快,建议使用独立的目录做为build context,只拷贝须要的文件到 build context 目录。
使用 .dockerignore 文件排除目录下不须要加载到 build context 内的文件或目录。
Docker 支持从stdin输入 dockerfile ,这种方式不会加载任何本地文件到docker的build context中。
Best practices for writing Dockerfiles
「Allen 谈 Docker 系列」docker build 的 cache 机制
Build secrets and SSH forwarding in Docker 18.09