一些关于Dockerfile的经验之谈

前言

最近都在给CI流程做改造, 又写了很多Dockerfile, 看看能不能给自己整理出一些零零碎碎的经验来.

容器安全

配置信息

docker build的时候, 一般都会需要带上一些认证信息, 最常见的就是要到私有的git仓库里面下载私有的依赖.

笔者都是在dockerfile中使用ARG命令去传参的. 而在执行docker build的时候, 则是利用jenkins管理的credentials去完成, 这样就可以让验证信息完全处于密文中, 保障了数据安全(起码不是明文).

而当构建出来的镜像要运行, 配置文件都通过configmap或者是volume去调用, 配置信息也就不需要放在镜像中了.

设置user

在默认情况下, Docker容器都会以root启动, 而容器里面的root跟宿主机里面的root是一样的, UID=0. 这样的隐患是, 当黑客进入到一个使用root去运行的容器时, 就等于获得了宿主机的root权限了. 而为了堵住这个隐患, 我们只需要在Dockerfile里面用USER命令指定一个非root用户就行.

如果遇到了一些坑爹场景, 必须要用root这个用户名去运行, 也可以使用Username re-map去避开.

设置时区

主要是一些业务上的考虑, 降低歧义或者是其他异常的可能.

警惕COPY * 陷阱

可以直接用一个小实验说明, 笔者简单创建了几个文件和文件夹:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@VM_110_57_centos dockerfile-test]# ls
1.txt 2.txt Dockerfile folder
[root@VM_110_57_centos dockerfile-test]# tree .
.
├── 1.txt
├── 2.txt
├── Dockerfile
└── folder
├── 3.txt
└── folder2
└── 4.txt

2 directories, 5 files

Dockerfile里面内容如下, 在里面, 使用了COPY *的操作:

1
2
3
4
from golang:1.20
WORKDIR /app
COPY * /app/
CMD ['sh']

如果你是个新手, 你也许会以为, 这个COPY *的操作, 会把当前文件夹里面的所有内容, 包括子文件夹都会复制到镜像里面, 我们验证一下.

1
2
3
4
5
6
7
# docker build -t rondochen/dockerfile-test:1.0 .
[root@docker]# docker run --rm -it rondochen/dockerfile-test:1.0 sh
# ls
1.txt 2.txt 3.txt Dockerfile folder2
# cd folder2
# ls
4.txt

从上面的输出, 我们就可以看到, COPY *的操作结果, 是会递归出当前文件夹里面的所有文件, 复制到镜像的WORKDIR中, 而且目录结构还会改变.

如果这是你本来的需求, 那你可以接着用. 但在笔者的工作场景中, 大多数时候, 当需要复制整个目录到镜像中, 在Dockerfile上是这样操作:COPY . ./.

指定版本号

在使用FROM指定基础镜像的时候, 不要用latest. 同样的道理, 如果需要在Dockerfile中使用apt或者yum时, 也最好指定要安装的软件的版本号.

大原则就是, 防止自动下载了一个让你出错的东西.

优先使用COPY而不是ADD

COPYADD都可以在构建过程中向容器添加文件, 在大多数情况下, 笔者会建议优先使用COPY. 因为ADD除了可以复制文件, 还能解压或者是下载URL等. 为了避免潜在的不确定性, 笔者会尽量不用ADD, 除非我很清楚我就是要通过URL下载一个文件或者有其他COPY命令不能完成的操作, 那才会使用ADD.

充分利用缓存

docker build的过程中, 从首行的FROM开始, docker程序会逐行遍历你的Dockerfile, 每一行的操作都会创建新的层. 但如果遇到一些重复操作, 距离上次的docker build没有任何变化, 那docker就会使用缓存直接完成这一次的操作.

有鉴于此, 在编排Dockerfile里面的内容的时候, 我们应该尽可能地把一些不会频繁变化的操作, 放到前面. 具体到实际的应用场景, 读者可以参考本站文章设计一个golang项目的容器构建流程: layer的概念. 再延伸到其他的开发框架, 我们都会有类似的场景, 比如在nodejs项目的容器构建过程中, 在Dockerfile中可以先执行npm install再把其他的项目文件放到容器中, 借此最大限度地复用cache, 节约时间.

但也要警惕缓存给你带来的陷阱, 比如一个单行的RUN apt-get update.

极简主义

尽量维持Dockerfile的简洁, 同时在项目规划的时候精简容器里面的内容. 下面是一些反例:

  • 一个镜像(容器)运行多个服务

  • 把数据库变更操作放在Dockerfile中执行

  • 把运行日志或者其他业务数据存放在容器内

  • 在容器内保留大量不必要内容, 如编译环境, 或者其他不必要的工具

  • 欢迎补充(笑)

留坑

还有一些在笔者的工作内容中没用到的, 但是面对一些特殊场景下会有用的命令, 这里先留一个坑:

  • HEALTHCHECK: 因为笔者所在的项目中, 已经有使用k8s自带的健康检查功能, 所以暂不使用

  • ONBUILD: 对比起使用ONBUILD命令去维护基础环境, 作为一个运维, 笔者更倾向于每个项目使用独立的multistage build dockerfile. 可能这个功能更适合用在开发阶段.

  • RUN –mount: 对于一些复杂蹊跷的环境比较适合

扩展阅读

Dockerfile reference

Best practices for writing Dockerfiles