Docker多阶段构建是17.05以后引入的新特性,旨在解决编译和构建复杂的问题。减小镜像大小。因此要使用多阶段构建特性必须使用高于或等于17.05的Docker。

什么情况下需要用到多阶段构建?

诸如Golang和Java的项目,需要编译后运行,如果在物理机上编译完再做成Docker镜像,那么也没什么问题。但如果要把构建和运行都放在Docker里完成,镜像就会很大了,构建使用的镜像可能是运行时的镜像的好几倍乃至几十倍。

多阶段构建可以解决这个问题,在第一阶段构建完成后将构建产物转移至第二阶段,同时丢弃第一阶段的镜像。这意味着不必在最终阶段前绞尽脑汁地优化Dockerfile了,Dockerfile中的每条指令都为图像添加了一个图层layer,为了优化镜像的大小,就需要记住在移动到下一层之前清理任何不需要的工件。

下面是一个Beego项目和一个Spring Boot项目的例子。

# 构建阶段
FROM golang AS build-env
ENV CGO_ENABLED 0
ENV GO111MODULE on
ENV GOPROXY https://goproxy.io
ADD . /go/src/app
WORKDIR /go/src/app

# 下载依赖以及打包
RUN go mod download && go get github.com/gadelkareem/bee@fix-mod
RUN bee pack -be GOOS=linux
RUN mkdir -v build && tar -zxvf app.tar.gz -C ./build

# 运行阶段
FROM alpine
COPY --from=build-env /go/src/app/build /app
WORKDIR /app
EXPOSE 8080
CMD [ "./app" ]
FROM gradle:jdk11 as builder
WORKDIR /builder
COPY . /builder
RUN gradle bootJar -x test

FROM adoptopenjdk:11-jre-hotspot
WORKDIR /app
COPY --from=builder /builder/build/libs/*.jar /app/halo.jar

ENV JVM_XMS="256m" \
    JVM_XMX="256m" \
    JVM_OPTS="" \
    TZ=Asia/Shanghai

RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone

ENTRYPOINT java -Xms${JVM_XMS} -Xmx${JVM_XMX} ${JVM_OPTS} -Djava.security.egd=file:/dev/./urandom -server -jar halo.jar

思路都是一致的,使用--from=?命令从任意阶段复制任何产物出来,参数是数字时,0代表从上到下第一个阶段,依次后推。
也可以用From ? AS ?给阶段命名,--from命令就可以使用阶段的别名,如上面实例所示。

--from不但可以从前置阶段中拷贝,还可以直接从一个已经存在的镜像中拷贝,例如:

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

上面是两个常用的框架的用法,但值得一提的是,docker官网给出了一个多阶段构建普通go应用的例子。

# syntax=docker/dockerfile:1
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

构建的时候关闭了CGO_ENABLED,这个东西有什么用,可以参考CGO_ENABLED环境变量对Go静态编译机制的影响,这里不多赘述。但是如果你的项目中使用到了需要CGO的依赖,比如sqlite,那么就需要添加额外的参数-ldflags '-linkmode "external" -extldflags "-static"',如下文,否则COPY到新阶段后的应用将无法执行。

RUN CGO_ENABLED=1 GOOS=linux go build -ldflags '-linkmode "external" -extldflags "-static"' -o app .