你有没有遇到过这种情况:写了个Go服务,编译完想打包成ref="/tag/2019/" style="color:#643D3D;font-weight:bold;">Docker镜像,结果发现镜像动不动就几百MB?明明程序本身才几MB,怎么一打包就这么胖?其实问题出在构建方式上,这时候就得用上Docker的“多阶段构建”了。
为什么镜像会变胖?
很多人习惯在一个Dockerfile里从头做到尾。比如先拉个golang镜像,把代码拷进去,编译,再运行。可问题是,golang镜像自带编译器和依赖库,体积很大,而你真正需要运行的只是一个编译好的二进制文件。把编译环境也塞进生产镜像,就像搬家时把厨房灶台也搬过去一样多余。
多阶段构建是怎么回事?
简单说,就是把构建过程分成几步,前一个“阶段”负责编译,后一个“阶段”只拿编译结果来运行。中间产物不保留,最终镜像自然就小了。
举个实际例子。假设你在开发一个用Go写的API服务,代码已经写好,现在要打包:
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
第一段用了golang镜像,起名叫builder,把代码编译成myapp;第二段换成了轻量的alpine系统,只把编译好的myapp复制进来。最终镜像大小可能从800MB直接降到15MB。
不止是Go,其他语言也能用
前端项目同样适用。比如用React写的页面,需要node环境打包,但最后只需要静态文件。你可以先用node镜像执行npm run build,再用nginx镜像把生成的dist目录放进去对外服务。
FROM node:18 AS frontend-builder
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
这样出来的镜像没有Node.js、没有源码、没有node_modules,只有Nginx和几个HTML、JS文件,干净又高效。
还能干点别的
多阶段不限于两个阶段。你可以设置多个中间阶段,比如一个专门测试,一个专门打包,一个专门发布。通过--from指定从哪个阶段拿文件,灵活得很。
另外,不同阶段可以用完全不同的基础镜像。编译Java用openjdk,运行时用jre或Alpine加glibc都行。只要最终能跑起来,怎么省怎么来。
现在很多CI流程也依赖这种写法。一条命令就能完成构建、测试、打包,不需要额外脚本搬运文件。对本地开发也友好,build完直接docker run验证,不用配一堆环境。