后端开发··2 阅读·预计 11 分钟

我让前端项目的 Docker 镜像从 1GB 瘦身到 50MB

从"能用就行"到"优雅生产",这是一次 Docker 优化之旅

去年接手了一个老项目,第一次 docker build 完,我看到镜像大小 1.2GB。作为有代码洁癖的工程师,我失眠了。经过一周的优化,最终把镜像压缩到 48MB。今天分享一下这个过程中的踩坑和经验。

为什么镜像大小很重要?

可能有人会说:"现在服务器存储这么便宜,1GB 又怎样?"

但镜像大小影响的不仅是存储:

  1. CI/CD 速度:镜像越大,构建、推送、拉取越慢。我们团队原本部署一次要 8 分钟,优化后只要 2 分钟
  2. 资源成本:K8s 集群中,每个节点都要拉取镜像,大镜像意味着更多带宽和时间
  3. 安全风险:镜像越大,包含的依赖越多,漏洞扫描的结果越吓人
  4. 启动速度:小镜像启动更快,对自动扩缩容很关键

第一版:从 1.2GB 开始

先看看最初的 Dockerfile:

FROM node:18

WORKDIR /app

COPY . .

RUN npm install
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

问题一目了然:

  • 用了完整的 node:18 镜像(基础镜像就 900MB+)
  • 把源码、node_modules、构建产物全塞进去
  • 没有多阶段构建

第二版:多阶段构建,降到 400MB

# 阶段1: 构建
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段2: 运行
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production

EXPOSE 3000
CMD ["npm", "start"]

改进点:

  • 使用 node:18-alpine(只有 40MB)
  • 多阶段构建,只复制构建产物
  • 只安装生产依赖

但还是 400MB?因为 Next.js 的 .next 目录本身就很大,而且还需要 node_modules 运行。

第三版:静态导出,拥抱 nginx

如果你的项目是纯前端(或者可以用 Next.js 静态导出),这是最优解:

# 阶段1: 构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm run export  # 生成静态文件到 out/

# 阶段2: 运行
FROM nginx:alpine
COPY --from=builder /app/out /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx 镜像只有 20MB,静态文件通常几十 MB,最终镜像 48MB

关键优化技巧

1. 选对基础镜像

# 差:完整镜像 900MB+
FROM node:18

# 好:alpine 变体 40MB
FROM node:18-alpine

# 极致:distroless 20MB
FROM gcr.io/distroless/nodejs18

但 alpine 有个坑:它是 musl libc,不是 glibc。某些 native 模块可能编译失败。

解决方案:

# 需要编译工具时
RUN apk add --no-cache python3 make g++

2. 智能利用缓存

# 错误:每次都重装依赖
COPY . .
RUN npm install

# 正确:利用 Docker 缓存
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Docker 是按层缓存的。package.json 不变时,不会重新安装依赖。

3. .dockerignore 是神器

node_modules
.next
out
dist
.git
.gitignore
*.md
.env.local
coverage
.idea

我见过有人把 .git 目录也 copy 进去,白白多了几百 MB。

4. 镜像分析工具

想知道镜像为什么大?用这个:

docker run --rm -it wagoodman/dive:latest your-image:tag

它会逐层分析,告诉你每一层包含什么文件,哪些可以优化。

Next.js 专项优化

如果你的 Next.js 项目必须用 SSR,无法静态导出:

方案一:Standalone 模式

next.config.js:

module.exports = {
  output: 'standalone',
}

构建后会生成一个独立可运行的最小化依赖包。

Dockerfile:

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

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]

最终镜像约 100MB,比原来的 400MB 好很多。

方案二:使用 serve 运行静态文件

如果页面可以预渲染:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:18-alpine
RUN npm install -g serve
WORKDIR /app
COPY --from=builder /app/out ./out
EXPOSE 3000
CMD ["serve", "-s", "out", "-l", "3000"]

实战踩坑记录

坑一:时区问题

alpine 镜像默认是 UTC 时区:

RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai

坑二:字体缺失

生成 PDF 或截图时发现中文乱码:

RUN apk add --no-cache fontconfig ttf-dejavu
# 或安装中文字体
RUN apk add --no-cache wqy-zenhei

坑三:健康检查

别忘了加健康检查:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

最终收益

优化后的成果:

指标优化前优化后提升
镜像大小1.2GB48MB96% ↓
构建时间8min2min75% ↓
部署时间3min30s83% ↓
漏洞数量127个3个97% ↓

总结

Docker 镜像优化的核心原则:

  1. 多阶段构建:构建和运行环境分离
  2. 选对基础镜像:优先 alpine/distroless
  3. 只带必需品:.dockerignore、生产依赖
  4. 利用缓存:合理安排 Dockerfile 指令顺序
  5. 选择合适的运行方式:静态导出 > standalone > 完整运行

如果你的镜像还很大,现在就打开终端 docker images,开始瘦身之旅吧!


这篇内容来自实战经验,希望对你有帮助。如有问题欢迎交流。

0 评论

评论区

登录 后参与评论