侧边栏壁纸
  • 累计撰写 113 篇文章
  • 累计收到 2 条评论

Docker 镜像体积从 1.2G 砍到 80M,我踩过的 5 个坑

2026-5-14 / 0 评论 / 55 阅读
🤖AI摘要
文章揭示了如何将Docker镜像体积从1.2G优化到80M的5个技巧:1. 使用轻量级基础镜像,如node:18-alpine;2. 避免使用COPY . .,优化Dockerfile结构;3. 使用多阶段构建减少镜像大小;4. 清理apt-get安装后的缓存;5. 使用.dockerignore排除不必要的文件。这些方法有效减少了镜像体积,加快了CI构建速度。

前几天在 CI 里看到构建日志,一个简单的 Node.js 应用镜像拉了我快两分钟。一看大小——1.2G。我当时就觉得不对劲,一个 Express 的 CRUD 后端凭什么这么大?

翻了一下 Dockerfile,前任(其实就是半年前的我)写的:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

这大概是大多数人的第一个 Dockerfile。能跑,但问题大了去了。

坑一:基础镜像选错了

node:18 是完整镜像,基于 Debian,自带 git、curl、gcc 一堆东西。你的生产环境根本用不到这些。

换成 node:18-slim 镜像直接从 900M 降到了 240M。再激进一点用 node:18-alpine,降到了 120M。Alpine 用的是 musl libc,某些 native 模块(比如 bcrypt)可能会炸,但大部分 Node 项目没问题。

如果只是跑静态编译的 Go 二进制?用 scratchalpine:latest,镜像能压到 10M 以内。

坑二:COPY . . 导致层缓存全废

Docker 的层缓存机制:每一行指令产生一个镜像层,只要这一行的输入没变,Docker 就会复用缓存。

COPY . . 的问题在于,你改了 README,改了 .gitignore,甚至改了个注释——整个 COPY 层都失效,后面的 RUN npm install 也得重新跑。

正确的做法是先把 package.json 和 lock 文件拷进去安装依赖,然后再拷源码:

COPY package*.json ./
RUN npm ci --only=production
COPY . .

这样只有 package.json 变了才会重新安装依赖。日常改代码只需要重建最后一层,CI 构建从 3 分钟变成 15 秒。

我第一次改完发现没快多少——因为忘了用 .dockerignore,node_modules 还是每次都拷进去了。

坑三:多阶段构建不是噱头

之前我觉得多阶段构建是"高级用法",没必要。直到有个 Go 项目镜像居然有 800M——因为把整个 Go SDK 都打包进去了。

多阶段构建的逻辑很简单:第一个阶段用完整的 SDK 编译出二进制;第二个阶段只拿这个二进制,放在一个干净的基础镜像里。Go 的例子:

FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:3.19
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]

这个镜像最后不到 15M,而没拆分之前是 800M+。

Node 项目也能用这招——build 阶段跑 npm run build,最终阶段只放 dist 目录和 production 依赖。

坑四:apt-get install 留下的垃圾

在 Dockerfile 里但凡跑过 apt-get update && apt-get install,就会留下 apt 缓存。这些缓存几百 MB 毫不夸张。

标准写法:

RUN apt-get update && \
    apt-get install -y --no-install-recommends ca-certificates curl && \
    rm -rf /var/lib/apt/lists/*

--no-install-recommends 跳过推荐包的安装,后面 rm -rf 把缓存干掉。这一行我自己踩过——没加这个清理,镜像凭空多了 200M。

坑五:.dockerignore 没配

这个是低级错误但我犯过不止一次。没配 .dockerignore,node_modules、.git、dist、*.log、DS_Store 全打包进镜像了。

一个基本的 .dockerignore:

node_modules
.git
dist
*.log
.env
.DS_Store
coverage

这个文件跟 Dockerfile 放在同一个目录,Docker 会自动读取。每次用 COPY . . 之前先检查 .dockerignore,能省很多体积。

加完 .dockerignore 后我的那个 Node 镜像又瘦了 40M——之前把整个 .git 目录都拷进去了。

我现在的 Dockerfile 模板

综合下来,我给自己攒了一个 Node.js 项目的 Dockerfile 模板:

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
RUN apk add --no-cache tini
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

加上 tini 是为了正确处理 SIGTERM——Docker 默认把信号发给 PID 1,但 Node 进程是子进程收不到的。tini 做 init 进程转发信号,K8s 里优雅下线就靠它。

效果

最开始那个 1.2G 的 Node 镜像,按上面的优化走下来是 82M。对,82MB。体积减了 93%,CI 构建从 3 分钟变 15 秒,部署时 kubelet 拉镜像的时间几乎感觉不到。

如果你也遇到 Docker 镜像太大的问题,建议从这三步下手:

  1. 先换 slim/alpine 基础镜像
  2. 加上 .dockerignore
  3. 上多阶段构建

这三步够覆盖 80% 的场景了。等这三步不够用了,再翻 docker-slim、dive 这些工具也不迟。


(我的项目是 Node 为主,Go 偶尔用。Java 的 jlink + jdeps 瘦身方案以后另外写一篇。)

评论一下?

OωO
取消