Docker(十五)—镜像优化

 

基础镜像

/* 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 .

image

多阶段构建

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 .
    

    image

使用经典的基础镜像

COPY –from 使用绝对路径 从上一个构建阶段拷贝文件时,使用的路径是相对于上一阶段的根目录的。如果你使用 golang 镜像作为构建阶段的基础镜像,就会遇到类似的问题。假设使用下面的 Dockerfile 来构建镜像

FROM golang
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 hello .
CMD ["./hello"]

image

这是因为 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"]

image

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"]

image

image

image

直接缩减到了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"]

image

从报错信息可以看出缺少文件,但没有告诉我们到底缺少哪些文件,其实这些文件就是程序运行所必需的动态库 那么该如何解决标准库的问题呢?有三种方案。

使用静态库

我们可以让编译器使用静态库编译程序,办法有很多,如果使用 gcc 作为编译器,只需加上一个参数-static

FROM gcc AS build
COPY hello.c .
RUN gcc -o hello hello.c -static
FROM scratch
COPY --from=build hello .
CMD ["./hello"]

image

image

可以看到镜像只有912K

拷贝库文件到镜像中

使用 busybox:glibc 作为基础镜像

有一个镜像可以完美解决所有的这些问题,那就是 busybox:glibc。它只有 5 MB 大小,并且包含了 glibc 和各种调试工具。如果你想选择一个合适的镜像来运行使用动态链接的程序,busybox:glibc 是最好的选择。 所以不建议使用 sratch 作为基础镜像,因为调试起来非常麻烦。