基础镜像
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}
/* hello.go */
package main
import "fmt"
func main () {
fmt.Println("Hello, world!")
}
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]
docker build -t hello:1.0 .
多阶段构建
FROM gcc AS build
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=build hello .
CMD ["./hello"]
- 基础镜像 gcc 来编译程序 hello.c
- 然后启动一个新的构建阶段,它以 ubuntu 作为基础镜像,将可执行文件 hello 从上一阶段拷贝到最终的镜像中。
docker build -t hello:2.0 .
使用经典的基础镜像
COPY –from 使用绝对路径 从上一个构建阶段拷贝文件时,使用的路径是相对于上一阶段的根目录的。如果你使用 golang 镜像作为构建阶段的基础镜像,就会遇到类似的问题。假设使用下面的 Dockerfile 来构建镜像
FROM golang
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 hello .
CMD ["./hello"]
这是因为 COPY 命令想要拷贝的是 /hello,而 golang 镜像的 WORKDIR 是 /go,所以可执行当然你可以使用绝对路径来解决这个问题,但如果后面基础镜像改变了 WORKDIR 怎么办?你还得不断地修改绝对路径,所以这个方案还是不太优雅。最好的方法是在第一阶段指定 WORKDIR,在第二阶段使用绝对路径拷贝文件,这样即使基础镜像修改了 WORKDIR,也不会影响到镜像的构建。例如:的真正路径是 /go/hello。
FROM golang
WORKDIR /src
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 /src/hello .
CMD ["./hello"]
FROM scratch 的魔力
回到我们的 hello world,C 语言版本的程序大小为 16 kB,Go 语言版本的程序大小为 2 MB,那么我们到底能不能将镜像缩减到这么小?能否构建一个只包含我需要的程序,没有任何多余文件的镜像?
答案是肯定的,你只需要将多阶段构建的第二阶段的基础镜像改为 scratch 就好了。scratch 是一个虚拟镜像,不能被 pull,也不能运行,因为它表示空、nothing!这就意味着新镜像的构建是从零开始,不存在其他的镜像层。
FROM golang
COPY hello.go .
RUN go build hello.go
FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]
直接缩减到了1.76M。
scratch的缺点
使用 scratch 作为基础镜像时会带来很多的不便。
缺少shell
scratch 镜像的第一个不便是没有 shell,这就意味着 CMD/RUN 语句中不能使用字符串
...
FROM scratch
COPY --from=0 /go/hello .
CMD ./hello
用 JSON 语法取代字符串语法。例如,将 CMD ./hello 替换为 CMD [”./hello”],这样 Docker 就会直接运行程序,不会把它放到 shell 中运行。
缺少调试工具
scratch 镜像不包含任何调试工具,ls、ps、ping 这些统统没有,当然了,shell 也没有(上文提过了),你无法使用 docker exec 进入容器,也无法查看网络堆栈信息等等。 折中一下可以选择 busybox 或 alpine 镜像来替代 scratch,虽然它们多了那么几 MB,但从整体来看,这只是牺牲了少量的空间来换取调试的便利性,还是很值得的。
缺少 libc
这是最难解决的问题。使用 scratch 作为基础镜像时,Go 语言版本的 hello world 跑得很欢快,C 语言版本就不行了,或者换个更复杂的 Go 程序也是跑不起来的(例如用到了网络相关的工具包)
FROM gcc AS build
COPY hello.c .
RUN gcc -o hello hello.c
FROM scratch
COPY --from=build hello .
CMD ["./hello"]
从报错信息可以看出缺少文件,但没有告诉我们到底缺少哪些文件,其实这些文件就是程序运行所必需的动态库 那么该如何解决标准库的问题呢?有三种方案。
使用静态库
我们可以让编译器使用静态库编译程序,办法有很多,如果使用 gcc 作为编译器,只需加上一个参数-static
FROM gcc AS build
COPY hello.c .
RUN gcc -o hello hello.c -static
FROM scratch
COPY --from=build hello .
CMD ["./hello"]
可以看到镜像只有912K
拷贝库文件到镜像中
使用 busybox:glibc 作为基础镜像
有一个镜像可以完美解决所有的这些问题,那就是 busybox:glibc。它只有 5 MB 大小,并且包含了 glibc 和各种调试工具。如果你想选择一个合适的镜像来运行使用动态链接的程序,busybox:glibc 是最好的选择。 所以不建议使用 sratch 作为基础镜像,因为调试起来非常麻烦。