把 Docker 镜像从 1.8GB 压到 87MB:一次真实的瘦身全过程
把 Docker 镜像从 1.8GB 压到 87MB:一次真实的瘦身全过程
接手一个项目,CI 流水线每次部署要等 12 分钟,其中 9 分钟耗在拉镜像上。查了一下,镜像 1.8GB。
花了一个下午优化到 87MB,拉取时间从 9 分钟降到 20 秒。这篇把每一步的数据和做法记录下来,都是可直接复用的。
## 一、先看清镜像里到底有什么
不看结构就动手,等于瞎猜。
```bash
docker history --no-trunc --format "table {{.Size}}\t{{.CreatedBy}}" myapp:latest | head -30
```
第一版输出让我有点意外:
```
1.2GB /bin/sh -c apt-get update && apt-get install -y ...
280MB COPY . /app
180MB /bin/sh -c npm install
60MB /bin/sh -c apt-get install -y python3 python3-pip
40MB COPY node_modules /app/node_modules
```
**1.2GB 那一层是罪魁祸首**:装了 build-essential、gcc、make 这些编译工具,但运行时根本用不到。
## 二、第一步:多阶段构建(1.8GB → 420MB)
核心思路:**编译环境和运行环境分离**。
```dockerfile
# ❌ 优化前
FROM node:18
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "dist/index.js"]
```
```dockerfile
# ✅ 多阶段构建
# ---------- 构建阶段 ----------
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---------- 运行阶段 ----------
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/index.js"]
```
关键点:
- `node:18-slim` 比 `node:18` 小 700MB(去掉了编译工具链)
- `--from=builder` 只复制产物,不复制中间文件
- `npm ci` 比 `npm install` 更快且可复现
这一轮从 1.8GB 降到 420MB。
## 三、第二步:换基础镜像(420MB → 180MB)
`node:18-slim` 还是带了不少东西。三个选择:
| 镜像 | 大小 | 适用 |
|---|---|---|
| node:18 | 1.1GB | 需要编译原生模块 |
| node:18-slim | 240MB | 大部分场景 |
| node:18-alpine | 130MB | 纯 JS,无 native 依赖 |
| gcr.io/distroless/nodejs18 | 110MB | 生产环境,无 shell |
我选了 alpine:
```dockerfile
FROM node:18-alpine AS builder
# ...构建...
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
```
**但踩了一个坑**:alpine 用 musl libc,某些 native 模块(如 `sharp`、`bcrypt`)编译出来的二进制在 glibc 环境跑不了。
```bash
# 报错长这样
Error loading shared library libstdc++.so.6: No such file or directory
```
解决方式:
```dockerfile
# 方案 1:装兼容层
RUN apk add --no-cache libstdc++
# 方案 2:换回 slim
FROM node:18-slim
# 方案 3:用预编译的二进制
RUN npm install --platform=linuxmusl --arch=x64 sharp
```
## 四、第三步:干掉 node_modules 里的垃圾(180MB → 92MB)
```bash
# 看 node_modules 里谁最大
du -sh node_modules/* | sort -rh | head -10
```
输出:
```
68M node_modules/.cache
24M node_modules/typescript
18M node_modules/@types
12M node_modules/jest
```
前三个都是**构建时才需要的**,运行时完全用不到。
```dockerfile
# 构建阶段装全部依赖
FROM node:18-alpine AS builder
COPY package*.json ./
RUN npm ci --include=dev
COPY . .
RUN npm run build
# 运行阶段只装生产依赖
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
```
关键:
- `RUN npm ci --omit=dev` 只装 dependencies,不装 devDependencies
- `npm cache clean --force` 删掉 npm 缓存(约 60MB)
- 把 `COPY package*.json` 和 `RUN npm ci` 分开,利用 Docker 层缓存
## 五、第四步:.dockerignore(92MB → 87MB)
这一条最容易被忽略,但效果立竿见影。
```bash
# 构建时把整个目录塞进去了
COPY . .
# .git、tests、docs 全进去了
```
```dockerignore
# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
README.md
docs/
tests/
*.md
coverage/
.vscode/
.idea/
```
配合 `COPY` 精确控制:
```dockerfile
# ❌ 粗暴复制
COPY . .
# ✅ 只复制需要的
COPY src/ ./src/
COPY package*.json ./
COPY tsconfig.json ./
```
## 六、第五步:合并 RUN 减少层数
每一条 `RUN` 都会产生一个新层,层数越多体积越大(因为每层都有元数据)。
```dockerfile
# ❌ 4 层
RUN apk add curl
RUN apk add bash
RUN rm -rf /var/cache/apk/*
RUN mkdir -p /app
# ✅ 1 层
RUN apk add --no-cache curl bash \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /app
```
注意 `--no-cache` 比 `rm -rf /var/cache/apk/*` 更彻底,前者根本不会写入缓存。
## 七、最终结果
```dockerfile
# ---------- 构建阶段 ----------
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# ---------- 运行阶段 ----------
FROM node:18-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
数据对比:
| 指标 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| 镜像大小 | 1.8GB | 87MB | **-95%** |
| 拉取时间 | 9 分 20 秒 | 20 秒 | **-96%** |
| 层数 | 24 层 | 8 层 | -67% |
| CI 部署总耗时 | 12 分钟 | 2 分 40 秒 | -78% |
| 安全漏洞(trivy 扫描) | 147 个 | 3 个 | -98% |
最后一项是意外收获:基础镜像越小,包含的 CVE 越少。1.8GB 的镜像里有 147 个高危漏洞,87MB 的只有 3 个。
## 八、几条经验
**1. 一定要用 docker history 开局**
不看层结构就优化,容易在错误的地方花时间。
**2. 多阶段构建是最划算的一步**
一次改动,体积直接降 75%,而且几乎零风险。
**3. alpine 不是万能药**
先确认有没有 native 依赖。有的话用 slim 更稳。
**4. 顺序很重要**
把变化频率低的指令放前面(`COPY package*.json` → `RUN npm ci` → `COPY src`),能吃满 Docker 层缓存,构建时间从 6 分钟降到 40 秒。
**5. 定期跑安全扫描**
```bash
trivy image myapp:latest --severity HIGH,CRITICAL
```
镜像瘦身不只是省带宽,更是缩小攻击面。
---
以上每一步都在真实项目验证过,Dockerfile 可直接参考。
- 点赞
- 收藏
- 关注作者
评论(0)