为什么在 Docker 中使用完全相同的构建命令每次都会产生不同的哈希值?

Why does using exactly the same build commands in Docker produce different hashes every time?

我正在 CircleCI 上构建一个简单的 CI 流程。它Docker创建一个 WordPress 实例,确定远程注册表是否有生成图像的副本,如果没有,则以特殊格式标记图像并推送它。

但是,我在实践中发现图像 总是 不同,我已经将问题追查到 Docker 文件的顶部,我在那里我正在调用 Alpine 的 apk 命令。完整的命令是:

RUN apk --update add git openssh-client

我已经完成了大约 20 分钟的两次构建,并且此命令在每种情况下都做了不同的事情。我从 Alpine 3.6 中的固定哈希开始。

这是倒数第二个版本的前几行:

Step 1/42 : FROM alpine@sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106 AS build
sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106: Pulling from library/alpine

Digest: sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106
Status: Downloaded newer image for alpine@sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106
 ---> 77144d8c6bdc
Step 2/42 : RUN apk --update add git openssh-client
 ---> Running in 4dee205378ad
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/community/x86_64/APKINDEX.tar.gz
(1/8) Installing ca-certificates (20161130-r2)
(2/8) Installing libssh2 (1.8.0-r1)
(3/8) Installing libcurl (7.59.0-r0)
(4/8) Installing expat (2.2.0-r1)
(5/8) Installing pcre (8.41-r0)
(6/8) Installing git (2.13.5-r0)
(7/8) Installing openssh-keygen (7.5_p1-r2)
(8/8) Installing openssh-client (7.5_p1-r2)
Executing busybox-1.26.2-r9.trigger
Executing ca-certificates-20161130-r2.trigger
OK: 28 MiB in 19 packages
 ---> 1c11addc5a9f
Removing intermediate container 4dee205378ad
Step 3/42 : WORKDIR /root
 ---> 0ec3661faedc
Removing intermediate container 3f0f8610abbc

这是最新版本:

Step 1/42 : FROM alpine@sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106 AS build
sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106: Pulling from library/alpine

Digest: sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106
Status: Downloaded newer image for alpine@sha256:3d44fa76c2c83ed9296e4508b436ff583397cac0f4bad85c2b4ecc193ddb5106
 ---> 77144d8c6bdc
Step 2/42 : RUN apk --update add git openssh-client
 ---> Running in 8ad903516136
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.6/community/x86_64/APKINDEX.tar.gz
(1/8) Installing ca-certificates (20161130-r2)
(2/8) Installing libssh2 (1.8.0-r1)
(3/8) Installing libcurl (7.59.0-r0)
(4/8) Installing expat (2.2.0-r1)
(5/8) Installing pcre (8.41-r0)
(6/8) Installing git (2.13.5-r0)
(7/8) Installing openssh-keygen (7.5_p1-r2)
(8/8) Installing openssh-client (7.5_p1-r2)
Executing busybox-1.26.2-r9.trigger
Executing ca-certificates-20161130-r2.trigger
OK: 28 MiB in 19 packages
 ---> 4192a8ae6ba6
Removing intermediate container 8ad903516136
Step 3/42 : WORKDIR /root
 ---> 9f2a57c9923b
Removing intermediate container 050a150cf83f

为了方便读者,这里有一个图形差异:

我的观点是,这种短期内的变化已经发生了太多次,以至于无法对旧版本的 Alpine(这是 3.6,最新的是 3.7)进行实际的 OS 更新。我认为它可能是在更新中写入数据或时间戳,这对于包管理器来说似乎是一件明智的事情。

但是,我希望在几分钟内创建的两个构建具有相同的哈希值,除非有实际的 OS 更新。有人会确认幕后可能发生的事情,或者解释我可以做些什么来检查实际的变化吗?例如,是否有一种明智的方法可以区分 Docker 层?

如果 apk 所做的更改永远无法完全重现,那么我最好的解决方案是在我的注册表中创建一个固定的基础映像,安装我需要的软件,然后在其上构建吗?我必须设置一些东西来定期重建它(例如,每隔几周)以获得安全更新,但如果它能让我获得稳定的构建层,那是可以接受的。

更新

我已将 apk 调用移动到单独的构建中,现在我发现 sed 命令正在更改哈希。

这是第一个运行:

Step 15/39 : FROM registry.gitlab.com/username/jonblog-machine:latest
 ---> 5f854fc73292
Step 16/39 : RUN sed -i -r 's/memory_limit = \d+M/memory_limit = 30M/g' /etc/php7/php.ini
 ---> Running in d7205d0216f6
 ---> b64f045b6f51

第二个运行:

Step 15/39 : FROM registry.gitlab.com/username/jonblog-machine:latest
 ---> 5f854fc73292
Step 16/39 : RUN sed -i -r 's/memory_limit = \d+M/memory_limit = 30M/g' /etc/php7/php.ini
 ---> Running in 152d628094ff
 ---> a397210c512b

这很令人费解:我在两者中都从 5f854fc73292 开始,但是第 16 步每次都会产生一个新的散列。 (它从构建 15 开始,因为这是一个多阶段构建)。

我怎样才能看到为什么我得到不同的哈希值?

层哈希包含文件的 mtime 时间戳,sed 命令会更改该时间戳。您可以在此处查看 OCI 图像的规范,docker 紧随其后:

https://github.com/opencontainers/image-spec/blob/master/layer.md#file-attributes