Dockerfile 最佳实践(翻译)

  • 一篇 Dockerfile 最佳实践博文翻译

  • 资料来源:

    https://www.qovery.com/blog/best-practices-and-tips-for-writing-a-dockerfile

  • 更新

    1
    2021.12.19 初始

导语

又是一篇填坑文,在计划中沉寂 N 久,以至于什么时候添加的都忘了…年底了,赶紧填坑..

编写 Dockerfile 最佳实践

Best Practices and Tips for Writing a Dockerfile 一文翻译,略去了一些 dockerfile 介绍,本文更偏向原则性内容.

编写用于生产环境的 dockerfile 并不是一件非常简单的事情,但是也并不一项令人生畏的工作,下面是编写 dockerfile 文件的最佳实践.

扫描安全漏洞

尽量对 docker 镜像镜像主动扫描,确定镜像不存在致命的漏洞.这方面已经存在几个现成的工具,包括 docker cli 内置的 Docker Scan.


题外话: Docker Scan 的免费额度是 10 次/月


扫描会评估镜像内安装的每一个系统包,将其与已知的 Common Vulnerabilities and Exposures (CVEs) 比较,并给出每个问题的修补建议.

最佳实践中少不了容器扫描,要避免已知的安全问题在没有人察觉情况下就滑入了生产环境.一般是在 CI 镜进行到镜像构建阶段时进行扫描,以避免开发人员无意中添加的风险包.

避免不必要依赖

docker 镜像应该是小之又小.精简 dockerfile 只保留最基本的东西.这样能够缩减镜像体积,加快构建速度,并减小攻击面.向托管服务或注册中心传输时也会需要较小的网络带宽.

不要安装任何应用程序不需要的包或库,一般来说我们很少与运行中的容器直接交互,因此没有必要添加以后可能用到的CLI工具等,这样的作法使得 dockerfile 专注在容器化你的应用程序而不是整个操作系统.

保护凭证

最常见也是最危险的 dockerfile 问题: 构建时包含了复制配置文件,确实构建时加入配置文件会省下不少功夫,但这是最应该避免的做法.

所有复制进 docker 镜像中的文件,任何能访问到镜像本体的都能获得.如果文件中恰好包含了数据库密码,那这些密码直接暴露给了所有用户.

将密钥或敏感数据提供给单个容器而不是镜像.使用环境变量/包含配置文件的卷/或者其他专用 docker 机制.在容器启动时注入数据,这可以避免意外的数据泄露,并确保镜像可以在不同环境中复用.

以非 root 用户运行

docker 容器通常默认以 root 用户运行,容器内 root 用户和宿主机 root 是一致的,这意味着如果利用容器内 web 服务,攻击者可以控制容器甚至宿主机.


注: 其实还是有点区别的,例如不特别提权,容器内 root 用户无法操作 iptabls.


USER 指令是可以指定容器运行的用户,输入是 用户名+GID,执行后,即使是在构建时,后面的构建指令全部都是以该用户运行.

1
USER demo-user:example-group

还有一种更大的隔离机制,即将 docker 的守护程序也以非 root 运行,这使得 docker 完全避免使用宿主机的 root,即使容器内一个进程被突破,攻击者也无法完全破坏宿主机,但以这样运行的 docker 有诸多的限制,并不适用于所有情况.

跟踪 dockerfile 更改

dockerfile 本质是文本文件,会随着时间一直更新.所以应该将他们提交到版本控制中.一些开发者将 dockerfile 和代码保存到同一个仓库,这样 git 拉取到本地后便于直接构建镜像.

使用版本控制可以让你跟踪所有 dockerfile 更改,如果构建了一个新镜像,部署时却失败了,有版本控制,会让你有一个可追踪错误的起点.

创建无状态 可重复的容器

docker 容器应该是短暂完全无状态的,而镜像是需要可重现的.每次运行 docker build 应该始终都使用同一个 dockerfile 文件重建一个完全相同的镜像.在 dockerfile 中需要指定软件包的确切版本,任何软件包都在不断更新,不指定版本,会使得即使同一份 dockerfile 文件,随着时间推移,得到的镜像也会有所区别.

可重复构建的一个特点是,不会对现有系统产生副作用.构建的目的是容器而不是数据库中的数据,这完全可以起一个单独的工作流程完成.

以上的做法使得备份数据更加容易,镜像是不用备份的,只需要有 dockerfile,即使 docker 镜像库崩了,也能快速构建出一模一样的镜像副本.

处理超长命令

dockerfile 中无法避免超长的命令,更少的行数意味着更少的层,对应的镜像体积会更小.

为了缓解这个问题,需要使用反斜杠组合多行,这也使得 dockerfile 更加易读.至于多行的顺序 docker 官方建议按照字母顺序排序,虽然这个原则无法在所有命令中实现,但是在安装软件包/下载文件时确实能提高可读性.

1
2
3
RUN apt-get update && \
apt-get install -y apache2 &&\
service apache2 restart

CMD ENTRYPOINT 区别

CMD 和 ENTRYPOINT 都是定义容器启动后运行进程的指令,ENTRYPOINT 设置容器启动时执行的进程,默认是一个 shell (/bing/sh). CMD 是为 ENTRYPOINT 进程提供默认参数.

1
ENTRYPOINT ["date"]CMD ["+%A"]

dockerfile 包含上面的指令,容器启动时会运行 date +%A.

设置自定义的 ENTRYPOINT 相当于是提供了一个默认值,使得用户可以快速访问到容器内的二进制文件,而不需要自己输入完整路径.

但使用 docker run 时,docker 会覆盖 CMD 但是会复用 ENTRYPOINT.

1
docker run my-image:latest +%B

这样执行的就是 date +%B 了.

使用 COPY 而不是 ADD

COPY 和 ADD 完成了一个相似的任务: 将文件添加到镜像,但是又有细微差别. COPY 只适用于本地文件,而 ADD 也接收 url 并自动打包成 tar,这使得使用 ADD 可能存在歧义. ADD archive.tarCOPY archive.tar 执行的结果可能完全不同,COPY 只是复制宿主机文件,而 ADD 有可能会直接打包文件.

因为 ADD 这个额外作用,使得需要从文件系统复制时,首选 COPY 防止歧义,这有利于向其他人传达改行命令的正确意义.

使用 STDIN 构建没有 dockerfile 的镜像

当然我们可以在没有 dockerfile 的情况下构建镜像,虽然这不是 docker 最佳实践,不过确实能够这样做,特别是作为 CI 流程一部分时,非常有用.

使用 docker build - 让 Docker 通过 STDIN 接收一个 dockerfile.

1
echo -e 'FROM ubuntu:latest\nRUN echo "Built from stdin"' | docker build -

总结

dockerfile 为容器构建提供了一种直观的可重复的方式,但要记住常见的问题和最佳实践,这样能够避免在生产环境的意外问题.

保持 dockerfile 尽可能小,积极扫描漏洞,确保没有敏感数据直接传入镜像,这些步骤将使得 dockerfile 保持可重复性.

当需要将镜像投入生产环境时,Qovery 可以将你的 dockerfile 直接部署到 AWS,安装 Qovery CLI,初始化一个新项目,之后一切交给 Qovery.

尾巴

这一篇算是一些常见原则的总结,更具体的做法,还有等后面的文章填坑.