注:我做了很多测试,发现不管是否使用 --link
,结果貌似都一样。我在网上搜了半天,最后发现,该功能貌似目前被disable了,参见 https://github.com/docker/buildx/issues/1099
。等什么时候可用了,做完测试,再来完善该文章。
总结
- 优点
- 时间:独立的layer,提高了缓存的复用性,节省时间
- 空间:向registry里push的image里只包含必要的layer,节省空间
cache-to
,cache-from
:todo
- 局限
- 不能依赖于前面layer的文件系统
- symlink
环境
- RHEL 9.3
- Docker Community 24.0.7
概述
COPY --link
是BuildKit的新功能,用来加速Docker image构建。它把文件复制到独立的image layer,并不需要上一步的layer存在。可以在base image不存在的情况下,为image添加新的内容。
该功能在作为Buildx v0.8的一部分,添加于2022年3月。Docker CLI的20.10.14版本包含了该功能。
“ --link” 是什么
--link
是Dockerfile的 COPY
指令的一个选项。
传统的 COPY
语句把文件复制到前一个layer里,该layer必须已存在,然后把新的内容合进来。
下面是一个传统的Dockerfile:
FROM alpine
COPY my-file /my-file
COPY another-file /another-file
- 在
FROM
指令之后,image包含了Alpine的内容:
bin/
dev/
etc/
......
- 第一个
COPY
指令创建了一个image,包含了Alpine的内容以及my-file
文件:
my-file
bin/
dev/
etc/
......
- 同理,第二个
COPY
指令在该image之上,添加了another-file
文件:
another-file
my-file
bin/
dev/
etc/
......
每条指令所创建的layer,包含了之前的所有东西和本指令新加的内容。在构建完成时,Docker使用一个“diff”进程检测出每个layer的变化。最终的image仅包含了在每个快照stage所添加的文件,但这并没有在构建的装配过程中体现出来(注:我理解这句话的意思是说,构建过程有一些冗余的东西)。
引入“ --link”
每次使用 --link
时,Docker会创建一个新的单独的文件系统。新文件不再复制到上一个layer里,而是复制到一个完全不同的位置,形成一个独立的layer。这些layer随后被链接在一起,产生最终的image。
下面是使用了 --link
的Dockerfile:
FROM alpine
COPY --link my-file /my-file
COPY --link another-file /another-file
FROM
指令不变,仍然创建了Alpine layer,包含了该image的所有内容:
bin/
dev/
etc/
......
- 第一条
COPY
指令这次创建了一个独立的layer,它是一个仅包含my-file
文件的新的文件系统:
my-file
- 同理,第二条
COPY
指令也创建了另一个新的快照,其中只包含another-file
文件:
another-file
当构建完成时,Docker把这些独立的快照保存为新的layer archive(tarball)。这些tarball被链接回之前的layer链里,构建出最终的image。它由这三个快照组合在一起。当创建容器时,其文件系统和原先(不用 --link
时)是一致的:
my-file
another-file
bin/
dev/
etc/
......
下图展示了这两种工作方式的不同:
使用“COPY --link”
COPY --link
只有在使用BuildKit构建image时才可以使用:
- 使用
docker buildx --create
构建,或者: - 构建时设置环境变量
DOCKER_BUILDKIT=1
此外,必须指定Dockerfile v1.4语法。
# syntax=docker/dockerfile:1.4
FROM alpine:latest
COPY --link my-file /my-file
COPY --link another-file /another-file
构建:
DOCKER_BUILDKIT=1 docker build -t my-image:latest .
构建好的image并无差异, --link
只影响构建过程。
示例
使用 --link
,即使复制的内容发生变化,也可以复用build cache。此外,即使base image不存在,也可以完成构建。
回到上面的例子。在添加新内容之前,标准的 COPY
行为要求 alpine
image必须已存在于宿主机上。如果之前没有pull过,则在构建时,该image会自动下载。
对于链接复制,Docker则不需要 alpine
image里的内容。它pull alpine 的manifest,为复制的文件创建新的独立layer,然后创建一个修订版的manifest,把这些layer链接到alpine的layer里。只有从新的image启动容器,或者导出tar achive时,alpine image的内容才会下载。当你push新image到registry时,registry只存储该image的新layer,并从远端获取alpine的layer。
该功能也有利于高效的image rebase。你可能正在使用当时最新的Ubuntu 20.04 LTS做Docker image:
FROM golang AS build
...
RUN go build -o /app .
FROM ubuntu:20.04
COPY --link --from=build /app /bin/app
ENTRYPOINT ["/bin/app"]
下面是一个完整的例子:
当前目录结构如下:
tree
.
├── Dockerfile
└── gotest1
├── go.mod
└── hello.go
1 directory, 3 files
go.mod
文件如下:
module test1
go 1.21.6
hello.go
文件如下:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Dockerfile
文件如下:
FROM golang AS build
WORKDIR /myproject
RUN --mount=type=bind,source=gotest1,target=gotest1 cd gotest1 && go build -o /app .
FROM ubuntu:20.04
COPY --link --from=build /app /bin/app
ENTRYPOINT ["/bin/app"]
在建构image时,你可以使用BuildKit的 --cache-to
选项来启用缓存。 inline
缓存在输出image里存储了构建的缓存数据,以便在后续的构建中重用。
docker buildx build --cache-to type=inline -t kaigotest1:20.04 .
注: buildx
可省略。
运行容器:
docker run kaigotest1:20.04
Hello, World!
假设,后来你想要使用Ubuntu 22.04:
FROM ubuntu:22.04
重新构建image时,可使用之前版本里嵌入的缓存数据:
docker buildx build --cache-from kaigotest1:20.04 -t kaigotest1:22.04 .
构建几乎瞬间就完成了。通过使用已有image里的缓存数据,Docker会验证构建 /app
的文件没有发生变化。这就意味着,通过 COPY
指令所创建的独立layer里的缓存仍然有效。由于该layer不依赖于其它layer, ubuntu:22.04
image也不会被pull下来。Docker仅仅在 ubuntu:22.04
layer链里,把包含 /bin/app
的快照layer链接到一个新的manifest里。快照layer被高效的“rebase”于一个新的parent image,而无需对文件系统的操作。
运行容器:
docker run kaigotest1:22.04
Hello, World!
该模型也可以优化multi-stage构建,其中任意stage之间都可能发生变化。
FROM golang AS build
RUN go build -o /app .
FROM config-builder AS config
RUN generate-config --out /config.yaml
FROM ubuntu:latest
COPY --link --from=config /config.yaml build.conf
COPY --link --from=build /app /bin/app
若没有 --link
,则产生的 config.yaml
文件的任何变化,都会导致 ubuntu:latest
被pull以及文件被复制。由于文件系统变化,造成缓存无效,还得重新编译。有了链接复制, config.yaml
文件的变化不会导致pull ubuntu:latest
或者重新编译。包含 build.conf
的快照layer简单的被替换为一个新的版本,这与其它layer是无关的。
什么情况不适用
有些情况下不适用 --link
选项。由于文件复制到一个新的layer,而不是添加到上一个layer里,所以在目标路径里,不能使用模糊引用(注:意指既可能是目录,也可能是文件,参见下面解释):
COPY --link my-file /data
在传统的 COPY
指令里,如果 /data
是image里已存在的一个目录,则 my-file
被复制为 /data/my-file
文件。而如果使用了 --link
,则目标layer的文件系统一定是空的,所以 my-file
一定会被复制到 /data
文件。
symlink(意指Linux的软链接文件)解析也有同样的考虑。标准的 COPY
指令会自动解析symlink的目标路径。而如果使用 --link
,则不支持该行为,因为symlink在copy的独立layer里不存在。
当没有遇到上述问题时,推荐使用 --link
。该功能会加速构建,并使得缓存更强大。由于这些非向后兼容的变化, --link
是一个选项(需显式指定),而不是缺省的功能。
总结
BuildKit的 COPY --link
使得构建更快更高效。使用链接复制的image无需pull之前的layer,仅仅是把文件复制过来。Docker会为每个 COPY
指令创建一个新的独立layer,然后把这些layer链接回layer链里。
参考
https://docs.docker.com/engine/reference/builder/#copy---link
https://www.howtogeek.com/devops/how-to-accelerate-docker-builds-and-optimize-caching-with-copy-link
https://www.docker.com/blog/image-rebase-and-improved-remote-cache-support-in-new-buildkit
(讲的非常清楚。另:文中貌似有个typo:COPY --from=build --link /out/myapp /bin
目标路径最后应该加上/
)https://github.com/docker/buildx/issues/1099