帆仔

16年入职网易,先后负责过多个重要手游项目;关注自动化、容器、云等方向;

在容器领域,docker 公司提出的容器镜像已经成为目前容器打包交付的事实标准。构建镜像需要编写 Dockerfile,如何编写一个优雅的 Dockerfile 呢?在 Docker 公司的官方文档中给出了一篇

Best practices for writing Dockerfiles。

(https://g.126.fm/03ncYHS)

本文在此基础上做了一些删减,力图让大家在短时间内写出一份不错的 Dockerfile。
本文分为三个部分,首先会直接给出一份 Dockerfile 的参考模板,然后说明如和构建高效的镜像并解释这个模板这样组织的原因,最后会补充说明一些编写过程中的常见问题。

一份简单的Dockerfile参考模板

docker 官方给出的参考文档中给出的 Dockerfile 指令接近 20 个,而我们平时在编写的时候,经常用到的不超过 10 个。因此,这里给出了一份 Dockerfile 的参考模板,几乎可以覆盖大部分的使用场景。

FROM base_image:tag    # 引用基础镜像 *必要*

ARG arg_key[=default_value1]     # 声明变量ENV env_key=value2     # 声明环境变量

# 构建几乎不变的部分,例如整体的目录结构,build时依赖的文件和工具包等COPY src dstRUN command1 && command2 ...

WORKDIR /path/to/work/dir   # 设置工作目录 

# 构建较少变动的部分,例如应用的依赖的文件、依赖的包等COPY src dstRUN command3 && command4 ...

# 构建经常变动的部分,例如应用的编译生成COPY src dstRUN command5 && command6 ...

# 容器入口  *必要*ENTRYPOINT ["/entry.app"]  # 指定容器启动时默认执行的命令CMD ["--options"] # 指定容器启动时默认命令的默认参数

构建高效镜像生命周期

容器的一个重要的特点就是能够快速迭代,因此在容器镜像迭代的各个环节也应该尽量做到简洁高效。

1. 镜像build

  • 精简 context:每次 build,context 都会复制给 docker daemon,因此要去掉 context 中无关的部分

  • 多层镜像:如果镜像很复杂,通常将其分成基础镜像(适用于多种应用,内容基本不变的部分)和应用镜像,应用镜像通过 FROM 基础镜像来减少 build 的步骤

  • 利用构建缓存(build cache):每次在 build 时,docker daemon 会默认从已在缓存中的父镜像开始,将下一条指令与从该基本镜像派生的所有子镜像进行比较,以查看是否其中一个是使用完全相同的指令构建的。如果不是,则缓存无效。因此,为了能够提高缓存的命中率,在编写 Dockerfile 时,应该尽量按照变动的频率来组织(如上文中的模板)

  • 减少 layers:RUN, COPY, ADD 等指令会在 build 时产生对应的 layer,在较旧的 Docker 版本中,需要最小化镜像中的层数以确保其性能。因此,使用&&来连接多个 RUN 命令是一个常用的方法(如上文中的模板)

  • 使用 multi-stage builds:新特性,后文会详细介绍

2. 镜像pull

docker 官方详细描述了 docker 镜像和容器在宿主机上的存储方式:https://docs.docker.com/storage/storagedriver/,简单来说就是:

  • 镜像层 ,只读,使用相同镜像的多个容器共用一份。镜像又按照 layers 分层:

    • 每层都有独立的 ID

    • 不同镜像如果有相同 ID 的 layer 时,共用一份

  • 容器层,可写,采用写时复制,容器在运行时修改的内容会在这一层

根据镜像的存储方式,我们也可以加快镜像的 pull 过程:

  • 多层镜像:和 build 时的分层镜像一样,利用本地已经存储的基础镜像来减少需要 pull 的 size

  • 利用 image layer 复用相同层:和 build 时利用缓存类似,利用本地已经存储的 layer 来减少需要 pull 的 size

  • 镜像预热:提前或空闲时 pull 镜像

常见问题

1. 注意Dockerfile中的指令是逐条执行,且相互独立

# 下面这种写法会报错,第二个RUN执行时的WORKDIR依旧是原来的目录,不是/some/dirRUN cd /some/dirRUN bash script.sh

# 改成下面两种之一RUN cd /some/dir && bash script.shRUN bash /some/dir/script.sh

2. 提防“过度”缓存

前文也提到过,Dockerfile 中每条指令逐条执行,且相互独立。大部分的指令在 build 时会生成对应的一层(layer),并被缓存。这种机制在绝大部分的情况下都工作的很好,但是有时也会产生问题:

# Dockerfile1FROM ubuntu:18.04RUN apt-get updateRUN apt-get install -y nginx

# Dockerfile2FROM ubuntu:18.04RUN apt-get updateRUN apt-get install -y nginx curl

如上,原 Dockerfile1 使用一段时间之后修改成 Dockerfile2(只修改了install这一行)。由于缓存机制(假设之前 build 的缓存还存在),Dockerfile2 在 build 时,update这一行不会真的执行,而是直接拿之前的缓存。此时安装的 nginx 和 curl 可能就不是当前的最新版本。

# 官方推荐的apt-get使用方式:RUN apt-get update && apt-get install -y \    curl \    nginx=1.16.* \    && rm -rf /var/lib/apt/lists/*

3. ARG与ENV

两种指令都可以用来定义变量,但是使用上有很多要注意的点:

  • FROM 前的 ARG 只能在 FROM 中使用,如果在 FROM 后也要使用,需要重新声明

ARG key=valueFROM xxx${key}xxxxARG key # 这里需要再次声明才能使用
  • ARG 变量的作用范围是 build 阶段 ARG 之后的指令,不会带入镜像

  • ENV 环境变量作用范围是 build 阶段 ENV 声明的指令,并且会编入镜像,容器运行时也会这些环境变量也生效

  • CMD 和 ENTRYPOINT 中不能使用 ARG 和 ENV 定义的变量

  • 当 ARG 和 ENV 变量同名时(无论是谁先定义),ENV 环境变量的值会覆盖 ARG 变量

  • ENV 会产生中间层(layer),被编入镜像,即使使用 unset 也无法去掉,例如:

FROM alpineENV ADMIN_USER="mark"  # 此时产生了layerRUN echo $ADMIN_USER > ./markRUN unset ADMIN_USER # 使用unset只是去掉了build时的环境变量,但是最终生成的镜像中还是会有这个变量

# 运行镜像还是会打印环境变量docker run --rm test sh -c 'echo $ADMIN_USER'mark

# 如果想要消除这种影响,可以改成:FROM alpineRUN export ADMIN_USER="mark" \    && echo $ADMIN_USER > ./mark \    && unset ADMIN_USERCMD sh

4. COPY与ADD

两个指令几乎相同,当你只想复制本地 context 中的文件到镜像中时,请无脑用 COPY

COPY 与 ADD 使用时,注意以下规则:

  • 注意文件的属性,复制时可以同时修改属主和属组 COPY/ADD [--chown=:]

  • 如果不清楚目录与反斜线对这两个指令的影响,对所有目录都加上反斜线就比较好理解了,如COPY / /,因为:

    • 是目录时,是否带反斜线都只会复制目录下的所有文件,不会复制目录本身,如果要复制目录本身,需要使用``的父目录

    • 是目录时,必须带反斜线才会把文件复制到dest下

  • 必须在 context 下,不能使用../跳出 context

ADD 指令除了 COPY 的所有功能外,还有以下特性,如非必要,尽量少用:

  • 是本地 tar 文件(常见的压缩格式)时,会自动解包

  • 可以是 url,支持从远程拉取

5. CMD与ENTRYPOINT

又是一对很类似的指令,使用时需要注意:

  • CMD 单独使用时,用来指定容器启动时默认执行的命令

  • ENTRYPOINT 单独使用时,可以完全取代 CMD

  • ENTRYPOINT 和 CMD 一起使用时,CMD 变成 ENTRYPOINT 的默认参数

  • 推荐使用 ENTRYPOINT/CMD 的 exec 书写形式:即ENTRYPOINT ["entry.app", "arg"],因为 shell 书写形式(ENTRYPOINT entry.app arg)会额外启动 shell 进程

下表列出了 CMD 与 ENTRYPOINT 的各种组合时的效果:

另外,通过在 docker run 最后的添加字段,可以指定 ENTRYPOINT 的实际参数

  # 镜像 test_entrypoint  ENTRYPOINT ["./entry.app"]  CMD ["--help"]

  # 运行 test_entrypoint  docker run test_entrypoint # 即./entry.app --help  # 带参数运行  docker run test_entrypoint -a -t  # 即 ./entry.app -a -t

6. multi-stage builds

Docker 17.05 之后的版本支持一种新的 build 方式:多阶段构建(multi-stage builds)。与传统方式的区别在与,多阶段构建能够使用多个 FROM 将整个 build 阶段分成多个阶段:

  • 通过为不同阶段命名,可以通过一份 Dockerfile 来管理 debug、test、product 等多种环境的镜像

  • 通过COPY --from=stage_name,来复制中间 stage 的文件到目标阶段,使得最终生成更小的镜像

例如,上文提到的模板就可以通过多阶段构建的方式来优化。假设我们最终只想得到 entry.app 及其运行环境,而不需要它的编译环境,那么可以通过如下方式优化最终生成的镜像的大小:

# 使用多阶段构建,这里命名一个builder阶段,生成编译后的appFROM base_image:tag AS builder   

ARG arg_key[=default_value1]     # 声明变量ENV env_key=value2     # 声明环境变量

# 构建整体的目录结构,build时依赖的文件和工具包等COPY src dstRUN command1 && command2 ...

WORKDIR /path/to/work/dir   # 设置工作目录 

# 构建编译环境COPY src dstRUN command3 && command4 ...

# 编译生成entry.appCOPY src dstRUN compile_entry_app

# 构建最终镜像的阶段,只保留应用和其运行环境,编译的依赖都不需要FROM base_image:tagCOPY src dest    # 复制运行环境WORKDIR /path/to/work/dir   # 设置工作目录 COPY --from=builder entry.app . # 从builder阶段复制app# 容器入口ENTRYPOINT ["/entry.app"]  # 指定容器启动时默认执行的命令CMD ["--options"] # 指定容器启动时默认命令的默认参数

往期精彩

NEW

ceph 部分数据所有副本先后故障的抢救

那些年,CDN 踩过的坑

智能监控中的时间序列预测

使用 d3.js 绘制资源拓扑图

运维里的人工智能

dockerfile arg_Dockerfile最佳实践相关推荐

  1. dockerfile构建镜像的命令_编写Dockerfile的最佳实践

    虽然 Dockerfile 简化了镜像构建的过程,并且把这个过程可以进行版本控制,但是很多人构建镜像的时候,都有一种冲动--把可能用到的东西都打包到镜像中.这种不正当的 Dockerfile 使用也会 ...

  2. 编写Dockerfile的最佳实践

    虽然 Dockerfile 简化了镜像构建的过程,并且让这个过程可以进行版本控制,但是很多人构建镜像的时候,都有一种冲动--把可能用到的东西都打包到镜像中.这种不正当的 Dockerfile 使用也会 ...

  3. Dockerfile最佳实践

    Docker 可以从 Dockerfile 中读取指令自动构建镜像,Dockerfile是一个包含构建指定镜像所有命令的文本文件.Docker坚持使用特定的格式并且使用特定的命令.你可以在 Docke ...

  4. Dockerfile制作jdk镜像和微服务镜像部署的最佳实践【Dockerfile实战】

    因为公司之前搭建测试服务器是我搭建的,其中包含使用docker来部署微服务项目,于是将这篇Dockerfile的最佳实践记录于此,为避免大家被坑,希望此文能帮你解除疑惑~ ps:因为是公司服务器不是个 ...

  5. ”微服务一条龙“最佳指南-“最佳实践”篇:Dockerfile

    这是"微服务一条龙"的项目第三篇,最近有很多网友私聊我希望我能够讲讲Dockerfile的相关注意事项,毕竟Dockerfile也是微服务部署的一个最最最-.基础的一个组件,大家不 ...

  6. Dockerfile 最佳实践

    之前 一篇文章介绍 docker 的镜像基本原理和概念 ,主要介绍在编写 docker 镜像的时候一些需要注意的事项和推荐的做法. 虽然 Dockerfile 简化了镜像构建的过程,并且把这个过程可以 ...

  7. Dockerfile最佳实践(二)

    本文讲的是Dockerfile最佳实践(二),[编者的话]本文是 Docker 入门教程第三章-DockerFile 进阶篇的第二部分.作者主要介绍了 Docker 的变化.常用指令以及基础镜像的最佳 ...

  8. DockerFile最佳实践:

    2019独角兽企业重金招聘Python工程师标准>>> 以下是我们列出的基本的DockerFile最佳实践: 保持常见的指令像MAINTAINER以及从上至下更新DockerFile ...

  9. Dockerfile最佳实践【原创、很多实践经验】

    首先,参见官方文档: dockerfile_best-practices 有如下几点说明:红色标注的是重点 Create ephemeral containers(构建无状态的容器) Understa ...

最新文章

  1. 分布式mysql中间件(mycat)
  2. python画笑脸-python 利用turtle库绘制笑脸和哭脸的例子
  3. OS / Linux / 伙伴(buddy)算法
  4. MapReduce案例一:天气温度
  5. localhost 就一定是 localhost 么?
  6. TikTok独立站该怎么布局?
  7. js bom dom
  8. multiply 和 dot 的区别
  9. 淘宝API代码c#实例(摘)
  10. U盘量产--U盘只读文件系统
  11. 2021年氯化工艺考试题库及氯化工艺考试试卷
  12. linux创建自签名证书
  13. ISP 接口隔离原则 Interface Seperate Principle
  14. SPOOLing和虚拟化
  15. 第一单元 用python学习微积分(五) 隐函数微分法和逆函数导数(下)- 反函数
  16. 智能灯丝灯方案为复古设计注入“ 科技基因 ”
  17. 学习编程语言的第一步,认识什么是计算机!!!
  18. oracle12c口令文件,学习笔记:Oracle 12C ASM 新特性 共享密码文件
  19. schannel: failed to receive handshake, SSL/TLS connection failed
  20. C语言随机数:rand()和srand(time(NULL))的使用

热门文章

  1. 用姓名字段统计人数_2019年度全国各地姓名报告分析汇总(全国、深圳、佛山、杭州)...
  2. LeetCode 32 最长有效括号
  3. C#——简单的计算器(仿Windows 10计算器)
  4. PHP——获取路径和目录
  5. CG CTF WEB bypass again
  6. mysql router 8.0.11_MySQL Router8
  7. 2012年度最受欢迎中国开源软件评选
  8. 2018年 第9届 蓝桥杯 Java B组 省赛真题详解及总结
  9. Android 应用目录分析
  10. ZBar 自定义界面