Docker镜像多阶段构建
目录
当我们采用容器化来部署项目时,对于大部分的程序(除开脚本语言)我们一般需要将源码编译成二进制的可执行文件,再将可执行文件打包成docker镜像。如果在一个Docker镜像中完成整个流程,那构建出来的镜像文件必然很大(包含了编译环境);如果把编译过程和最终的可执行文件拆分为两个镜像,又会增加维护成本。那么有什么方法来解决这个问题呢?
背景
在构建Docker镜像时,一个最大的挑战是如何减小目标镜像的体积。
在Dockerfile中,每一行指令(ADD、COPY、RUN)会给镜像增加一个新的层,所以一般情况下我们需要在当前层将下一层不再使用的内容清除掉,而这需要利用shell的各种小trick和一些特殊的写法。
在开头提到的问题中,先来看看两个镜像的做法,其中一个大镜像包含完整的编译环境,另一个小镜像只包含最终的可执行文件(用于生产环境运行)。因此,我们需要维护两个Dockerfile。
hello.go
package main
import (
"fmt"
)
func main(){
fmt.Println("hello world")
}
Dockerfile.build
FROM golang:1.17
WORKDIR /go/src/app
COPY hello.go .
RUN go build -o hello hello.go
Dockerfile
FROM alpine:latest
WORKDIR /app
COPY hello .
CMD ["./hello"]
build.sh
#!/bin/bash
echo Building hello-build
docker build -t myhello:build . -f Dockerfile.build
docker container create --name extract myhello:build
docker container cp extract:/go/src/app/hello .
docker container rm -f extract
echo Building hello-go
docker build --no-cache -t myhello:latest .
最终执行:
$./build.sh
$docker run --rm myhello
hello world
可以看到,即使是编译一个简单的程序,我们需要维护的Dockerfile和脚本也是如此复杂。
多阶段构建
利用Docker的多阶段构建,我们可以在一个Dockerfile中多次使用 FROM
语句,每一个FROM意味着使用不同的基础镜像并且开始一个新的构建,而且我们还可以直接在多个构建之间复制需要的文件。这样,我们就只需要维护一个Dockerfile。
FROM golang:1.17 AS builder
WORKDIR /go/src/app
COPY hello.go .
RUN go build -o hello hello.go
FROM alpine:latest
WORKDIR /app
COPY --from=builder /go/src/app/hello .
CMD ["./hello"]
构建镜像并运行:
$docker build -t myhello:latest .
$docker run --rm myhello
hello world
这样子整个流程就简单多了,我们不再创建中间镜像也不再需要将可执行文件复制到本地环境中。
在指定 --from
的时候,我们还可以使用外部镜像,比如:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
构建指定阶段
如果我们的Dockerfile中包含多个构建,我们可以指定只构建某一部分,比如:
$docker build --target builder -t myhello:build .
这种方式在某些场景下会非常有用:
- 调试某个特定的构建;
- 使用
debug
来构建带有调试符号信息的程序和工具,而不影响production
; - 使用
testing
来构建带有测试数据的环境,而production
始终使用真实数据;
参考:https://docs.docker.com/develop/develop-images/multistage-build/
评论