Docker 容器化部署
Docker 是前端 SSR / Node 服务部署的事实标准。掌握 Dockerfile + docker-compose 是 2025+ 后端/全栈的必备技能。
一、避坑指南
用
node:alpine而不是node:latest:# ❌ 错误:latest 会随时间变化,构建不可复现
FROM node:latest
# ✅ 正确:固定版本 + 固定小版本
FROM node:20-alpine必须用多阶段构建:
# ❌ 错误:单阶段 → 镜像 1.5GB(含 node_modules 全部)
FROM node:20
WORKDIR /app
COPY . .
RUN pnpm install
RUN pnpm build
CMD ["node", "dist/server.js"]
# ✅ 正确:多阶段 → 镜像 150MB(只含 prod deps + 产物)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
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"]必须用
WORKDIR而不是RUN cd:# ❌ cd 只在当前 RUN 生效
RUN cd /app && pnpm install
# ✅ WORKDIR 持久
WORKDIR /app
RUN pnpm install.dockerignore必加(避免传node_modules、.git等):.git
node_modules
.next
.env*
!.env.example
dist
coverage
*.log
.DS_Store以
non-root用户运行(安全):# ❌ 默认 root,容器逃逸风险
USER root
# ✅ 用 node 用户
USER nodeCOPY 顺序影响缓存:
# ❌ 改一行代码就重装所有依赖
COPY . .
RUN pnpm install
# ✅ 先 COPY package 文件 → 装依赖 → 再 COPY 源码
COPY package*.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .不要把
ARG用作 secret:# ❌ ARG 会留在镜像历史里,docker history 能看到
ARG API_KEY=xxx
ENV API_KEY=$API_KEY
# ✅ 用运行时 ENV(`docker run -e`)或 secrets
ENV API_KEY=${API_KEY} # 由 docker run -e 注入
二、什么时候用 Docker?
| 场景 | 用 Docker? | 推荐方案 |
|---|---|---|
| 静态站点(Vite 构建产物) | ❌ 没必要 | 直接上 CDN(OSS + CDN) |
| SSR 服务(Next.js / Nuxt) | ✅ 首推 | Docker + Nginx / K8s |
| Node 后端服务(NestJS / Express) | ✅ 首推 | Docker + PM2 / K8s |
| 多容器协作(前端 + 后端 + DB) | ✅ | docker-compose |
| 纯 SPA 演示 | 可选 | 简单就用 Vercel / Netlify |
| 单机小服务 | 可选 | PM2(更轻) |
决策原则:SSR / Node 服务用 Docker;纯静态用 CDN。
三、Docker vs PM2 vs K8s
| 维度 | Docker | PM2 | Kubernetes |
|---|---|---|---|
| 定位 | 容器化打包 | Node 进程管理 | 容器编排 |
| 适用规模 | 单机/小集群 | 单机 | 大规模集群 |
| 部署粒度 | 容器 | 进程 | Pod |
| 资源隔离 | ✅ 强(namespace/cgroup) | ❌ 无(共享 OS) | ✅ 强 |
| 自动恢复 | 需 docker restart 策略 | ✅ 内置 | ✅ 内置 |
| 负载均衡 | 需 Nginx/HAProxy | ✅ 内置 cluster | ✅ Service / Ingress |
| 学习成本 | 中 | 低 | 高 |
| 适用团队 | 任何 | 小团队 / 内部工具 | 中大规模 |
进阶组合:Docker 打包 → K8s 编排。PM2 用于没有 K8s 时的「单机多进程」。
四、Dockerfile 实战
4.1 Next.js(App Router)
完整 Dockerfile(基于 standalone 输出):
# ---- 1. 依赖阶段 ----
FROM node:20-alpine AS deps
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile
# ---- 2. 构建阶段 ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# ---- 3. 运行阶段 ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# 用非 root 用户
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
next.config.ts 需开启:
const nextConfig = {
output: "standalone", // ← 关键:减小镜像体积
};
4.2 Vite 静态站 + Nginx
# ---- 1. 构建阶段 ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build # 输出到 dist/
# ---- 2. Nginx 托管 ----
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA 路由 fallback
location / {
try_files $uri $uri/ /index.html;
}
# 长缓存
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 不缓存 HTML
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
4.3 NestJS
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
五、docker-compose
多容器协作(前端 + 后端 + DB + Redis)用 docker-compose 编排。
# docker-compose.yml
version: "3.9"
services:
web:
build: .
image: my-app:latest
container_name: my-web
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:pass@db:5432/mydb
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
container_name: my-db
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: my-redis
restart: unless-stopped
volumes:
postgres_data:
docker compose up -d # 后台启动
docker compose logs -f web # 看日志
docker compose down # 停掉
docker compose ps # 看状态
六、镜像优化
| 优化项 | 效果 | 做法 |
|---|---|---|
| 多阶段构建 | 1.5GB → 150MB | builder + runner 分开 |
| Alpine 基础 | 1GB → 150MB | node:alpine |
| .dockerignore | 避免传无用文件 | 加 node_modules .git 等 |
| 依赖缓存 | 改代码不重装依赖 | 先 COPY package 文件 |
| distroless | 150MB → 80MB | gcr.io/distroless/nodejs20 |
| layer 合并 | 减少 layer 数 | RUN apt-get update && apt-get install 在同一 RUN |
distroless:Google 出的极简镜像(无 shell、无包管理器),仅含运行时。安全 + 小巧,但调试麻烦。
七、CI 集成(GitHub Actions)
- name: Build & Push 镜像
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha # 用 GitHub Actions 缓存
cache-to: type=gha,mode=max
- name: 部署到服务器
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /app
docker compose pull web
docker compose up -d web
八、面试高频
Q1:多阶段构建解决了什么?
A:单阶段构建把构建工具(dev deps、TypeScript、webpack 等)全留在最终镜像,镜像 1.5GB+。多阶段用 builder 跑构建,只把产物复制到 runner,镜像缩到 150MB 左右。也是镜像安全的必要手段(构建工具不在生产环境暴露)。
Q2:为什么用 Alpine?
A:Alpine 是基于 musl libc 的极简 Linux,基础镜像 ~5MB(vs Debian 80MB+)。适合 Node/Go/Rust 等自带 libc 的应用。但 Alpine 有兼容性陷阱(sharp、canvas 等需要 glibc 的库要改用 node:slim)。
Q3:ENTRYPOINT 和 CMD 区别?
A:
CMD是默认命令,可被docker run <image> other-command覆盖ENTRYPOINT是主命令,传参会追加而非覆盖
# CMD:可被覆盖
CMD ["node", "server.js"]
# docker run myapp ls → 跑 ls,CMD 被覆盖
# ENTRYPOINT:参数追加
ENTRYPOINT ["node", "server.js"]
# docker run myapp --version → 跑 node server.js --version
Q4:Docker volume 是什么?
A:绕过容器文件系统的持久化机制。容器删除时数据会丢,volume 把数据存到宿主机。三种类型:
- 命名 volume(推荐,Docker 管理)
- 绑定挂载(开发时挂代码到容器)
- tmpfs(内存,敏感数据)
Q5:Docker 镜像和容器什么关系?
A:镜像 = 类,容器 = 实例。镜像是只读模板,容器是镜像的运行时实例(多了可写层)。一个镜像可以起 N 个容器。