在Docker 17.05之后,Docker在构建中支持了多阶段构建,简单来说,Dockerfile中可以有多个FROM,这篇文章通过一个简单的示例来说明多阶段构建的使用场景和方式。
- 基本语法
- 场景说明
-
- 代码示例
- 构建go语言应用
- 构建go应用镜像
- 运行go应用容器
- 上述场景的其他解法
-
- 解法示例Dockerfile
- 分阶段方式的构建
- 运行go应用容器
- 总结
FROM的三种语法格式如下所示:
语法格式1:FROM [--platform=][AS]
语法格式2:FROM [--platform=][:] [AS]
语法格式3:FROM [--platform=][@] [AS]
场景说明比如有如下go语言的类似spring boot的web应用,提供在8080端口提供信息显示服务。此应用构建时需要go语言的编译环境,而一旦运行的时候则只需要一个Alpine容器即可,一般来说可以分两步进行,先编译,编译完生成的二进制文件再进行构建生成可执行的容器。
代码示例liumiaocn:go liumiao$ cat basic-web-hello.go package main import "fmt" import "net/http" func Hello(response http.ResponseWriter, request *http.Request) { fmt.Fprintf(response, "Hello, LiuMiao, Welcome to go web programming...\n") } func main() { http.HandleFunc("/", Hello) http.ListenAndServe(":8080", nil) } liumiaocn:go liumiao$构建go语言应用
执行命令:docker pull golang:1.15.3-alpine3.12
可以看到此编译环境即使是alpine方式也是不小的,如果运行时使用此镜像作为Base镜像大小比较浪费,如果用在边缘设备上基本上不太可行。
liumiaocn:go liumiao$ docker images |grep 1.15.3-alpine3.12 golang 1.15.3-alpine3.12 d099254f5fc3 7 days ago 299MB liumiaocn:go liumiao$
使用此镜像进行构建
liumiaocn:go liumiao$ docker run --rm -it -v `pwd`:/build golang:1.15.3-alpine3.12 sh /go # cd /build /build # ls basic-web-hello.go /build # go build -o http-server basic-web-hello.go /build # ls basic-web-hello.go http-server /build #
可以看到已经成功构建出来所需要的二进制文件http-server了
构建go应用镜像此处使用一个Alpine镜像即可运行此go语言应用了,我们这里使用如下简单的Dockerfile来实现
liumiaocn:go liumiao$ cat Dockerfile FROM alpine:3.12.1 WORKDIR /target COPY http-server /target/ EXPOSE 8080 ENTRYPOINT ["/target/http-server"] liumiaocn:go liumiao$
然后即可进行镜像的构建了
liumiaocn:go liumiao$ docker build -t test-go:latest . Sending build context to Docker daemon 6.453MB Step 1/5 : FROM alpine:3.12.1 ---> d6e46aa2470d Step 2/5 : WORKDIR /target ---> Running in da7d4cae785c Removing intermediate container da7d4cae785c ---> ae974a6c826c Step 3/5 : COPY http-server /target/ ---> 3fce2c8c9c1c Step 4/5 : EXPOSE 8080 ---> Running in dc98c1a16510 Removing intermediate container dc98c1a16510 ---> 395a0c569fb1 Step 5/5 : ENTRYPOINT ["/target/http-server"] ---> Running in e30b500061ea Removing intermediate container e30b500061ea ---> da9281ce2a5f Successfully built da9281ce2a5f Successfully tagged test-go:latest liumiaocn:go liumiao$
可以看到构建的镜像大小也比较适中
liumiaocn:go liumiao$ docker images |grep test-go test-go latest da9281ce2a5f 2 minutes ago 12MB liumiaocn:go liumiao$运行go应用容器
这里使用docker run命令运行此容器,也可以使用其他各种方式,比如docker-compose或者在kubernetes中
liumiaocn:go liumiao$ docker run --rm -p 8080:8080 test-go
在另外的终端或者浏览器中进行确认此8080端口启动的go的web应用
liumiaocn:go liumiao$ curl http://localhost:8080 Hello, LiuMiao, Welcome to go web programming... liumiaocn:go liumiao$
可以看到已经可以正常运行了
上述场景的其他解法上述场景是一个非常常见的开发过程的编译、构建和运行阶段的操作,比较典型的就是构建阶段所用到的很多依赖和环境在最终的结果中可能只是部分需要,所以导致的问题就是要分开来做,构建产生二进制文件,然后将此二进制文件拿出来然后再进行构建,而在Docker 17.03之后的这个版本,现在有了新的解法了。
解法示例Dockerfile首先来看一个示例解法,使用如下Dockerfile可以直接解决此问题
liumiaocn:go liumiao$ cat Dockerfile ARG GOLANG_VER=1.15.3 ARG ALPINE_VER=3.12 FROM golang:${GOLANG_VER}-alpine${ALPINE_VER} as go-builder-1.15.3 WORKDIR /build COPY basic-web-hello.go /build RUN cd /build && go build -o http-server basic-web-hello.go FROM alpine:${ALPINE_VER} WORKDIR /target COPY --from=go-builder-1.15.3 /build/http-server /target/ EXPOSE 8080 ENTRYPOINT ["/target/http-server"] liumiaocn:go liumiao$
可以看到这个Dockerfile和普通的Dockerfile略有区别,就是感觉是两个Dockerfile拼接在一起的,后续的Dockerfile中对于前面的关联仅仅在于所生成的二进制文件,它的好处?IAAC算不算一个回答?算。原本需要手工操作才能完成的连接工作,将此二进制文件和其相应的Dockerfile进行关联,但是考虑到比如此二进制文件的版本、支持的操作系统的体系架构、权限设定、传输产生的不完整性风险等,以及相关的Dockerfile的版本变化以及人为的误操作因素等,这么小的一个点也可能产生很多问题。而在一个Dockerfile中就没有这个问题了,因为输入的go语言应用,输出的是最终的应用镜像,没有中间的其他结果,那我们来看一下这种方式下的构建过程。
分阶段方式的构建liumiaocn:go liumiao$ docker build -t go-app . Sending build context to Docker daemon 3.072kB Step 1/11 : ARG GOLANG_VER=1.15.3 Step 2/11 : ARG ALPINE_VER=3.12 Step 3/11 : FROM golang:${GOLANG_VER}-alpine${ALPINE_VER} as go-builder-1.15.3 ---> d099254f5fc3 Step 4/11 : WORKDIR /build ---> Running in 64c975a9affe Removing intermediate container 64c975a9affe ---> 11f7f0fed685 Step 5/11 : COPY basic-web-hello.go /build ---> e0fae581e05b Step 6/11 : RUN cd /build && go build -o http-server basic-web-hello.go ---> Running in 025158d79879 Removing intermediate container 025158d79879 ---> 1cb815aa110f Step 7/11 : FROM alpine:${ALPINE_VER} ---> d6e46aa2470d Step 8/11 : WORKDIR /target ---> Using cache ---> ae974a6c826c Step 9/11 : COPY --from=go-builder-1.15.3 /build/http-server /target/ ---> 7671328fc17e Step 10/11 : EXPOSE 8080 ---> Running in 9db998af6571 Removing intermediate container 9db998af6571 ---> cf5362c2063a Step 11/11 : ENTRYPOINT ["/target/http-server"] ---> Running in d51adf74a6e2 Removing intermediate container d51adf74a6e2 ---> 0dfeb1ca46df Successfully built 0dfeb1ca46df Successfully tagged go-app:latest liumiaocn:go liumiao$
可以看到构建的过程包括了编译和最终结果的生成,而我们所关心的是这个镜像是否像我们期待的那样没有包括go的编译环境里面几百兆的内容呢?执行docker images可以看到
liumiaocn:go liumiao$ docker images |grep go-app go-app latest 0dfeb1ca46df 7 seconds ago 12MB liumiaocn:go liumiao$
正是我们所期待的结果,接下来我们运行一下此go应用,看看是否能够正常动作。
运行go应用容器同样这里也使用docker run命令运行此容器
liumiaocn:go liumiao$ docker run --rm -d -p 8080:8080 go-app 3f3b951caab2c91f00157966571aae6c90c08029b65e80dbb8d91f1b49888228 liumiaocn:go liumiao$ docker ps |grep go-app 3f3b951caab2 go-app "/target/http-server" 6 seconds ago Up 4 seconds 0.0.0.0:8080->8080/tcp trusting_shamir liumiaocn:go liumiao$
在另外的终端或者浏览器中进行确认此8080端口启动的go的web应用
liumiaocn:go liumiao$ curl http://localhost:8080 Hello, LiuMiao, Welcome to go web programming... liumiaocn:go liumiao$
可以看到已经可以正常运行了
总结实际上分阶段构建主要应用了Docker 17.03之后的FROM as语法,如果as省略的话from参数可以直接使用0这样的需要进行引用,结合起来还是能够实现很多有用的功能的。从本质上来说Dockerfile是单根模式的,但是在实际使用中,有时会出现可能会需要两个不同的Base镜像的内容的可能,但是在本质上并没有改变其单根的特点,比如本文中最终所生成的go语言应用,它的根是Alpine镜像,它继承了Alpine镜像的所有内容,但是同时又需要另外一个镜像中的某个文件,这实际上是一个非常常见的场景,也可以看出Docker也是一直根据用户需求在不断地进行功能演化。