Skip to main content

Docker 容器化部署

Docker 是前端 SSR / Node 服务部署的事实标准。掌握 Dockerfile + docker-compose 是 2025+ 后端/全栈的必备技能。

一、避坑指南

  1. node:alpine 而不是 node:latest

    # ❌ 错误:latest 会随时间变化,构建不可复现
    FROM node:latest

    # ✅ 正确:固定版本 + 固定小版本
    FROM node:20-alpine
  2. 必须用多阶段构建

    # ❌ 错误:单阶段 → 镜像 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"]
  3. 必须用 WORKDIR 而不是 RUN cd

    # ❌ cd 只在当前 RUN 生效
    RUN cd /app && pnpm install

    # ✅ WORKDIR 持久
    WORKDIR /app
    RUN pnpm install
  4. .dockerignore 必加(避免传 node_modules.git 等):

    .git
    node_modules
    .next
    .env*
    !.env.example
    dist
    coverage
    *.log
    .DS_Store
  5. non-root 用户运行(安全):

    # ❌ 默认 root,容器逃逸风险
    USER root

    # ✅ 用 node 用户
    USER node
  6. COPY 顺序影响缓存

    # ❌ 改一行代码就重装所有依赖
    COPY . .
    RUN pnpm install

    # ✅ 先 COPY package 文件 → 装依赖 → 再 COPY 源码
    COPY package*.json pnpm-lock.yaml ./
    RUN pnpm install --frozen-lockfile
    COPY . .
  7. 不要把 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

维度DockerPM2Kubernetes
定位容器化打包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 → 150MBbuilder + runner 分开
Alpine 基础1GB → 150MBnode:alpine
.dockerignore避免传无用文件node_modules .git
依赖缓存改代码不重装依赖先 COPY package 文件
distroless150MB → 80MBgcr.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 个容器

九、相关文档