Docker 镜像瘦身实战:从 1.2GB 压到 85MB 的完整过程

Docker 镜像瘦身实战:从 1.2GB 压到 85MB 的完整过程

镜像 1.2GB 不是"功能多",是你把构建机上的垃圾原封不动打包发给了全世界。

去年我给一个 Node 服务做部署优化,拉一次镜像要 10 分钟,服务器磁盘动不动就爆。压完之后镜像只剩 85MB,拉取从 10 分钟降到 40 秒,冷启动快了一倍。这篇把整个过程和每个手段的收益拆开讲,你可以照着对自己的项目做一遍。

一、先搞清楚镜像为什么大

镜像是由一层层只读层叠出来的,每一层里的文件即使后面删掉,也还留在那一层里。所以"先 COPY 再 RM"不减体积,"同一层里装完就删"才减。常见的体积来源:

来源 典型占比 说明
基础镜像选错 30%~60% node:20 约 1GB,node:20-alpine 只有 130MB
构建依赖被打进运行层 20%~40% devDependencies、编译工具链(gcc/make)
源码与构建上下文 10%~30% .git、测试、文档、node_modules 被 COPY 进去
包管理器缓存 5%~15% apt/pip/npm 的缓存目录没清理

先用 docker history 镜像名 看每层大小,或者装个 dive 逐层分析,找到最大的几层再动手,比盲改有效得多。

bash 复制代码
docker images                    # 看总大小
docker history myapp:latest      # 看每层是谁贡献的
dive myapp:latest                # 逐层浏览,标记浪费

二、三板斧:多阶段构建 + 换基础镜像 + .dockerignore

1. 多阶段构建(收益最大)

思路很简单:编译阶段需要 gcc、devDependencies、源码,运行阶段只需要产物。让构建在第一阶段发生,第二阶段只拷贝产物。

优化前的典型错误 Dockerfile:

dockerfile 复制代码
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/main.js"]

这一个文件把上面表格里的坑全踩了:基础镜像 1GB、COPY . ..git 和本机 node_modules 都带进来、devDependencies 全量安装。

优化后:

dockerfile 复制代码
# ---- 构建阶段:随便装,反正不进最终镜像 ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci                       # 先拷 package.json 装依赖,充分利用层缓存
COPY . .
RUN npm run build
RUN npm prune --omit=dev         # 只留生产依赖

# ---- 运行阶段:干净的 alpine,只放产物 ----
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]

这一个改动通常就能砍掉 60%~80% 体积。注意两个细节:

  • 先 COPY package.json 再装依赖、最后才 COPY 源码------这样改代码不会击穿依赖缓存层,构建从几分钟回到几秒。
  • npm ci 代替 npm install,版本严格按 lock 文件来,避免"我本地好好的"。

2. 基础镜像选型

镜像 大小 适合
node:20 ~1GB 不推荐,除非离不开 glibc 专属依赖
node:20-slim ~200MB 需要 Debian 生态、glibc 二进制依赖
node:20-alpine ~130MB 纯 JS 依赖首选,最小

换 alpine 有一个常见翻车点:依赖里有原生二进制包 (如 sharpbcryptsqlite3)时,alpine 用 musl 而不是 glibc,可能装不上或运行报错。两个解法:改用官方提供 musl 预编译版本的包(bcryptjs 替代 bcrypt),或退回 -slim。Python 项目同理,python:3.12-slim 是比 alpine 更稳妥的默认选择。

3. .dockerignore:把垃圾挡在构建上下文外

COPY . . 之前没有 .dockerignore,等于把本机目录整个塞给 Docker。在项目根目录加:

text 复制代码
.git
node_modules
dist
build
coverage
*.log
.env*
Dockerfile
.dockerignore
README.md

两个关键点:node_modules 必须忽略(本机和容器架构不同,混用必炸);.env* 必须忽略(我见过把生产数据库密码打进公开镜像的事故)。

三、依赖与缓存清理的细节

Node 项目

  • npm ci --omit=devnpm prune --omit=dev,把 devDependencies 从运行层踢出去,这一步通常省 100~300MB。
  • 如果必须 npm install,同一层里装完就清缓存:RUN npm install && npm cache clean --force

Python 项目

dockerfile 复制代码
FROM python:3.12-slim AS base
RUN pip install --no-cache-dir -r requirements.txt

--no-cache-dir 一个参数省几十到上百 MB。编译型依赖(如 psycopg2)在 slim 里要 apt-get install gcc,记得装完立刻删:

dockerfile 复制代码
RUN apt-get update && apt-get install -y --no-install-recommends gcc \
    && pip install psycopg2 \
    && apt-get purge -y gcc && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*

所有清理必须写在同一个 RUN 里------分开写就是两个层,前面装的东西还是留在镜像里。

Go 项目(最爽)

Go 的多阶段构建收益最夸张,产物是静态二进制:

dockerfile 复制代码
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app .

FROM gcr.io/distroless/static
COPY --from=builder /app /app
ENTRYPOINT ["/app"]

最终镜像可以压到 10~20MB,连 shell 都没有,攻击面也小。

四、优化前后对比

我那个 Node 服务的实测数据:

优化手段 镜像体积 累计降幅
原始(node:20 + COPY . .) 1.2GB ---
换 alpine + .dockerignore 620MB -48%
多阶段构建 + omit=dev 110MB -91%
加 --no-cache-dir / 清缓存 85MB -93%

拉取时间从 ~10 分钟降到 ~40 秒(按 10Mbps 带宽算),服务器磁盘占用直接少一个数量级,CI 里构建缓存命中后整个镜像构建只要 30 秒。

五、几个容易忽略的坑

  1. latest 标签陷阱 :永远给镜像打明确版本或 git sha(myapp:20260918-a1b2c3),回滚时你才会感谢自己。
  2. 时区与 locale :alpine 默认 UTC,日志时间对不上,加 RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  3. 健康检查 :镜像瘦身后别丢运维能力,HEALTHCHECK CMD wget -qO- http://localhost:3000/health || exit 1(alpine 里没有 curl,用 wget)。
  4. 别为了瘦身砍掉 curl/wget 后又需要调试 :需要临时进容器排查时用 docker debug(新版)或临时起一个带工具的 sidecar,不要把调试工具永久打进生产镜像。
  5. CI 里也要清理 :构建机 docker system prune -f 定期跑,否则磁盘爆的是构建机不是镜像。

六、总结

镜像瘦身不是玄学,就是一句话:运行时需要什么就放什么,其他全部留在构建阶段 。动手顺序建议------先看 docker history 找大头,再上多阶段构建,然后换 alpine/slim,最后补 .dockerignore 和缓存清理。今晚就可以拿自己项目试一遍,90% 的项目能砍掉 70% 以上的体积,而这一切不需要你改一行业务代码。

相关推荐
小马同学-1 小时前
Containerd入门:架构原理与安装部署
容器·containerd
溪语流沙3 小时前
Django + Vue电商项目第001讲:开篇|注册登录加增删改查,那不是电商
redis·python·mysql·docker·typescript·django·vue
我爱吃土豆14 小时前
从零给个人项目搭一套 CI/CD
阿里云·部署·cicd
一直在努力学习的菜鸟5 小时前
Docker Info 详细解析(Rocky Linux 8.10 / Docker 29.8.1)
docker
小马同学-5 小时前
crictl实战:K8s容器运行时排障工具
容器·k8s·crictl
逐流人6 小时前
Containerd容器管理实战:从架构原理到nerdctlcrictl工具链
linux·运维·云原生·容器·云计算·containerd
AlfredZhao6 小时前
Docker 删除 none 镜像:一次真实的清理记录
docker
bosins15 小时前
WSL2 启动即崩溃?调整超时参数解决 Docker 阻塞问题
linux·docker·wsl
闲云自留地16 小时前
Containerd 进阶教程:命名空间 Namespace、私有仓库 Harbor、容器管理全流程
容器