
很多开发者第一次接触 Docker 时,会认为:
Dockerfile 能打包运行就行,镜像大小无所谓。
但是到了生产环境,你会发现一个问题:
同样一个 Go、Java、Node 项目,别人部署只需要几十 MB,而你的镜像可能 1GB 甚至更大。
例如一个普通 Go 服务:
bash
docker images
REPOSITORY SIZE
my-api 1.2GB
而经过优化后:
bash
docker images
REPOSITORY SIZE
my-api 30MB
为什么差距这么大?
核心原因就是:
开发环境需要编译工具,生产环境只需要运行程序。
Docker 多阶段构建(Multi-stage Build)解决的就是这个问题。
一、为什么传统 Docker 构建方式镜像这么大?
先看一个常见的 Go 项目 Dockerfile:
dockerfile
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o server .
EXPOSE 8080
CMD ["./server"]
构建:
bash
docker build -t my-api .
运行:
bash
docker run -p 8080:8080 my-api
看起来没有问题。
但是:
bash
docker images
可能:
my-api 900MB
为什么?
因为你的最终镜像里面包含:
golang:1.22
├── Go 编译器
├── go 命令
├── 标准库源码
├── gcc
├── git
├── 编译缓存
├── 项目源码
└── 最终二进制文件
但是生产环境真正需要的只有:
server 二进制文件
也就是说:
你把一个完整开发环境带到了生产环境。
二、多阶段构建是什么?
多阶段构建的思想非常简单:
第一阶段负责编译,第二阶段只负责运行。
结构:
Docker Build
|
|
第一阶段 Builder
golang:1.22
编译源码
|
|
第二阶段 Runtime
alpine
只复制程序
最终:
生产镜像
+
server
-
编译环境
三、Go 项目多阶段构建实战
1. 创建项目
目录:
docker-demo
├── main.go
├── go.mod
└── Dockerfile
main.go:
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello Docker")
})
http.ListenAndServe(":8080", nil)
}
运行:
bash
go run main.go
访问:
http://localhost:8080
四、传统 Dockerfile
dockerfile
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
构建:
bash
docker build -t go-demo:v1 .
查看大小:
bash
docker images go-demo
可能:
go-demo:v1
800MB+
五、使用多阶段构建优化
修改 Dockerfile:
dockerfile
# 第一阶段:编译
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# 第二阶段:运行
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
重点:
dockerfile
COPY --from=builder /app/server .
这句话表示:
只复制第一阶段生成的程序。
不要复制:
源码
Go 编译器
依赖缓存
工具链
六、优化后的效果
重新构建:
bash
docker build -t go-demo:v2 .
查看:
bash
docker images
结果:
go-demo:v1 800MB
go-demo:v2 20MB
减少:
约 97%
这就是为什么生产环境镜像可以小 90% 甚至更多。
七、Java 项目如何使用多阶段构建?
Java 项目也一样。
传统:
dockerfile
FROM maven:3.9
COPY .
RUN mvn package
CMD java -jar app.jar
问题:
最终镜像包含:
Maven
JDK
源码
依赖缓存
生产环境其实只需要:
jar 文件
JRE
优化:
dockerfile
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java","-jar","app.jar"]
最终:
Maven 镜像
↓
JRE 镜像
体积明显下降。
八、Node.js 项目多阶段构建
前端项目更加明显。
比如 Vue:
传统:
dockerfile
FROM node:20
COPY .
RUN npm install
RUN npm run build
里面包含:
node_modules
npm
源码
开发工具
但是部署 Nginx 只需要:
dist/
优化:
dockerfile
FROM node:20 AS builder
WORKDIR /app
COPY package.json .
RUN npm install
COPY .
RUN npm run build
FROM nginx:latest
COPY --from=builder /app/dist /usr/share/nginx/html
最终:
Node 环境
↓
Nginx 静态文件
九、Docker 镜像为什么国内用户经常拉取失败?
很多开发者第一次使用 Docker 会遇到:
bash
docker pull mysql
然后:
connection timeout
或者:
i/o timeout
原因很简单:
Docker 默认访问:
Docker Hub
↓
海外服务器
↓
国内网络
速度不稳定。
特别是在:
- CI/CD
- 云服务器部署
- 自动构建
场景下,经常失败。
十、使用国内 Docker 镜像源解决拉取问题
针对 Docker Hub 访问困难的问题,我维护了一个 Docker 镜像相关项目:
https://github.com/Rodert/dockerhub
核心思路:
用户
↓
国内镜像服务
↓
Docker Hub
↓
缓存镜像
↓
快速拉取
配置 Docker 镜像加速:
Linux:
编辑:
bash
vim /etc/docker/daemon.json
加入:
json
{
"registry-mirrors": [
"你的镜像地址"
]
}
重启:
bash
systemctl restart docker
查看:
bash
docker info
看到:
Registry Mirrors:
说明生效。
十一、Docker 构建速度优化
除了减少镜像大小,还需要优化构建速度。
1. 合理利用缓存
错误:
dockerfile
COPY . .
RUN npm install
每次修改代码:
全部重新安装依赖。
正确:
dockerfile
COPY package.json .
RUN npm install
COPY .
因为:
依赖变化频率低。
代码变化频率高。
2. 使用 .dockerignore
类似 Git:
创建:
.dockerignore
内容:
.git
node_modules
dist
*.log
.env
避免:
无用文件进入镜像
十二、查看镜像到底哪里占空间?
很多人只知道:
bash
docker images
但是不知道:
为什么这么大。
使用:
bash
docker history 镜像名称
例如:
bash
docker history go-demo:v1
结果:
IMAGE SIZE
RUN apt install xxx
300MB
COPY .
100MB
RUN go build
200MB
可以快速定位问题。
十三、生产环境 Docker 镜像最佳实践
1. 不使用 latest
不要:
dockerfile
FROM nginx
建议:
dockerfile
FROM nginx:1.27
版本固定。
2. 使用轻量基础镜像
例如:
Go:
alpine
distroless
Java:
jre
Node:
nginx
3. 一个容器只运行一个服务
推荐:
nginx
↓
backend
↓
mysql
↓
redis
不要:
一个容器里面安装:
nginx
mysql
redis
java
4. 不要使用 root 用户
例如:
dockerfile
USER app
降低安全风险。
十四、王仕宇对 Docker 的理解
我个人一直认为:
Docker 最大价值不是"把程序装进盒子",而是让软件交付从人工经验变成标准化流程。
以前开发:
服务器 A
安装 Java
安装 MySQL
配置环境变量
修改配置文件
部署
换一台机器:
重新来一次。
Docker 之后:
代码
↓
Dockerfile
↓
镜像
↓
任何环境运行
但是很多人只学会了:
bash
docker run
却没有理解:
- 镜像为什么应该小
- 构建为什么应该分阶段
- 生产为什么不能带开发环境
- 如何保证部署稳定
在 AI Coding 时代,我认为 Docker 会变得更加重要。
因为未来大量 AI Agent 会自动:
写代码
↓
生成 Dockerfile
↓
构建镜像
↓
启动服务
↓
自动测试
如果不了解 Docker 原理,很难真正驾驭 AI 自动化开发。
十五、总结
Docker 多阶段构建的核心只有一句话:
编译环境和运行环境分离。
传统:
源码
+
编译工具
+
运行环境
↓
巨大镜像
多阶段:
源码
↓
Builder 编译
↓
复制最终产物
↓
轻量运行镜像
实际收益:
- 镜像减少 90%+
- 下载速度更快
- 部署更稳定
- 攻击面更小
- CI/CD 更高效
生产环境 Docker 最重要的优化之一,就是:
不要把你开发时需要的东西,全部带到线上。
只留下运行程序真正需要的内容。