前端也要懂 Docker:一次搞定部署环境差异
上季度部署事故复盘:新同事本地 Node 16 打包的产物上到 Node 18 的 CI 里跑构建,一个依赖的原生模块编译差异导致白屏。排查半天。会后我做了件事:把前端构建部署全面 Docker 化,从此"环境差异"这个词在我们的部署字典里消失了。
这篇写给和我一样"知道 Docker 但没亲手用过"的前端同学。
为什么前端需要 Docker
前端产物明明是静态文件,也要 Docker?核心原因:不一致的不是运行时,是构建过程。
- 本地 Node 版本 vs CI 的 Node 版本 vs 服务器的 Node 版本,三个环境三种行为
- 依赖里的原生模块(node-sass、sharp)在不同平台/版本下编译结果不同
- 构建环境差异导致的问题最难排查,因为"代码一模一样"
Docker 的答案是:把构建环境本身也固化成镜像的一部分。任何机器上 build,跑的都是同一个 Node 版本、同一份依赖。
核心概念三分钟速成
只讲前端要用的四个:
- 镜像(Image):环境的快照,相当于"装好 Node 18 + pnpm 的系统光盘"
- 容器(Container):镜像跑起来的实例
- Dockerfile:描述"怎么做出这个镜像"的配方
- 多阶段构建(multi-stage):一个 Dockerfile 里分几步,前面阶段装依赖构建、最后阶段只留产物——前端镜像必用
一个真实可用的 Dockerfile
以我们的 Nuxt 项目为例:
# ===== 阶段一:构建 =====
FROM node:22-alpine AS builder
# 装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
# 先只复制依赖清单,利用 Docker 层缓存
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# 再复制源码并构建(源码变化不会让依赖层缓存失效)
COPY . .
RUN pnpm build
# ===== 阶段二:运行 =====
FROM node:22-alpine
WORKDIR /app
# 只从构建阶段拿产物,镜像里没有源码和 devDependencies
COPY --from=builder /app/.output ./.output
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
几个关键设计:
1. alpine 变体:node:22-alpine 只有 50MB 左右(完整版 300MB+),镜像小拉取快。注意 alpine 用 musl libc,个别原生模块可能有兼容问题,遇到了换 node:22-slim。
2. 分层缓存的顺序:先 COPY package.json + lock 装依赖,再 COPY 源码。Docker 每一层有缓存——只改了源码时,依赖层直接命中缓存,构建从 5 分钟变 40 秒。顺序写反(先 COPY .)是新手最常见的错误。
3. 多阶段构建:最终镜像只含 .output 产物,构建依赖(几百 MB 的 node_modules、源码)全留在 builder 阶段不带进最终镜像。我们的镜像从 1.2GB 瘦到 180MB。
构建与运行
# 构建镜像,打标签
docker build -t my-blog:1.0.0 .
# 本地跑起来验证
docker run -d -p 3000:3000 --name blog my-blog:1.0.0
# 传环境变量
docker run -d -p 3000:3000 \
-e NUXT_JWT_SECRET=xxx \
-e NUXT_SITE_URL=https://blog.example.com \
my-blog:1.0.0
本地验证 OK 后推到镜像仓库,服务器拉下来就跑——服务器上连 Node 都不用装,有 Docker 就行。
.dockerignore 别忘了
node_modules
.nuxt
.output
.git
*.log
.env
不配这个,COPY . . 会把本地 node_modules(可能几百 MB)整个塞进构建上下文,又慢又大。
docker-compose:本地一键起全家桶
项目依赖 MySQL + Redis 时,compose 一条命令拉起整套环境:
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=mysql://root:root@db:3306/blog
depends_on:
- db
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=blog
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
docker compose up,应用和数据库一起起来,新同事入职第一天就能跑通全栈环境。
接入 CI/CD
GitHub Actions 里的流程变成:
- name: 构建并推送镜像
run: |
docker build -t registry.example.com/blog:${{ github.sha }} .
docker push registry.example.com/blog:${{ github.sha }}
- name: 服务器部署
run: |
ssh deploy@server "docker pull registry.example.com/blog:${{ github.sha }} && \
docker stop blog && docker rm blog && \
docker run -d --name blog -p 3000:3000 registry.example.com/blog:${{ github.sha }}"
用 commit sha 做镜像标签,每个提交对应唯一镜像,回滚就是换个标签重新 run。
小结
- 前端用 Docker 的核心动机:固化构建环境,消灭"我本地好好的"
- Dockerfile 三要点:alpine 瘦身、先依赖后源码吃缓存、多阶段构建只留产物
- .dockerignore 排除 node_modules
- 镜像标签用 commit sha,回滚零成本
- compose 一键拉起本地全栈环境
Docker 化之后,我们的部署事故率直线下降,新环境从"配一天"变成"run 一条命令"。前端同学们,这个技能真的值得点亮。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~