正在加载,请稍候…

Node.js 应用的 Docker 最佳实践:生产就绪容器

学习如何正确地将 Node.js 应用容器化,涵盖多阶段构建、安全加固、镜像优化、docker-compose 设置及生产部署策略。

Docker Best Practices for Node.js Applications: Production-Ready Containers

Node.js 应用的 Docker 最佳实践:生产就绪容器

将 Node.js 应用容器化看似简单——直到你遇到镜像体积过大、安全漏洞或构建缓慢等问题。这些最佳实践将帮助你构建快速、小巧且安全的 Docker 镜像。

基础 Dockerfile(及其问题)

# ❌ 简单粗暴的方法——不要在生产中使用
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "src/index.js"]

问题:

  1. 镜像超过 1GB(完整 Node.js + 开发依赖)
  2. 从主机复制 node_modules(或每次都重新构建)
  3. 以 root 用户运行(安全风险)
  4. 没有层缓存优化
  5. 生产镜像中包含开发依赖

Docker Best Practices for Node.js Applications: Production-Ready Containers illustration

最佳实践 #1:多阶段构建

# ✅ 多阶段构建——将构建与运行分离
# 阶段 1:构建阶段
FROM node:20-alpine AS builder
WORKDIR /app

# 仅复制依赖文件(缓存优化)
COPY package*.json ./
RUN npm ci --include=dev  # 安装包括 devDependencies 用于构建

# 复制源码并构建
COPY . .
RUN npm run build  # TypeScript 编译、打包等

# 阶段 2:生产镜像
FROM node:20-alpine AS production
WORKDIR /app

# 仅复制生产依赖
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# 从构建阶段复制构建产物
COPY --from=builder /app/dist ./dist

# 安全:不要以 root 运行
USER node

EXPOSE 3000
CMD ["node", "dist/index.js"]

结果:约 150MB 镜像,而非 1GB+

最佳实践 #2:使用 Alpine 镜像

# 镜像大小对比:
# node:20           ~ 1.1GB
# node:20-slim      ~ 247MB
# node:20-alpine    ~ 133MB  ← 使用这个

FROM node:20-alpine

# Alpine 使用 apk 而非 apt
RUN apk add --no-cache dumb-init

# dumb-init 正确处理信号(用于优雅关闭)
CMD ["dumb-init", "node", "dist/index.js"]

最佳实践 #3:优化层缓存

Docker 会缓存每一层。更改一层会使所有后续层失效。

# ❌ 不好:在 npm install 之前复制所有内容
COPY . .           # 任何文件更改都会变化
RUN npm install    # 总是重新安装!

# ✅ 好:先复制依赖,最后复制源码
COPY package*.json ./
COPY tsconfig.json ./    # 如果构建需要
RUN npm ci               # 缓存直到 package.json 变化!

COPY src/ ./src/         # 仅当 src 变化时使缓存失效
RUN npm run build

Docker Best Practices for Node.js Applications: Production-Ready Containers illustration

最佳实践 #4:安全加固

FROM node:20-alpine

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs &&     adduser -S nodeapp -u 1001 -G nodejs

WORKDIR /app

COPY --chown=nodeapp:nodejs package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --chown=nodeapp:nodejs dist/ ./dist/

# 切换到非 root 用户
USER nodeapp

# 暴露非特权端口
EXPOSE 3000

# 只读文件系统(可选,非常安全)
# 在 docker run 中添加:--read-only --tmpfs /tmp

CMD ["node", "dist/index.js"]

最佳实践 #5:.dockerignore

# .dockerignore — 类似于 .gitignore
node_modules/
npm-debug.log
dist/
.git/
.gitignore
README.md
*.md
*.test.ts
*.spec.ts
coverage/
.env
.env.*
docker-compose*.yml
Dockerfile*

健康检查

FROM node:20-alpine
# ... 其他指令 ...

# 健康检查——Docker 在不健康时重启容器
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

# 或者如果 curl 可用
# HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "dist/index.js"]
// 在 Express 应用中添加 /health 端点
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    version: process.env.APP_VERSION || '1.0.0',
  });
});

Docker Best Practices for Node.js Applications: Production-Ready Containers illustration

用于开发的 Docker Compose

# docker-compose.yml
version: '3.9'

services:
  api:
    build:
      context: .
      target: builder  # 开发阶段使用 builder 阶段(包含 devDeps)
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src  # 挂载源码以实现热重载
      - /app/node_modules  # 不要从主机挂载 node_modules
    environment:
      - NODE_ENV=development
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: npm run dev  # nodemon 用于热重载
    
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      
  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  postgres_data:

用于生产的 Docker Compose

# docker-compose.prod.yml
version: '3.9'

services:
  api:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - .env.production
    restart: unless-stopped
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
    depends_on:
      - db
      - redis
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - api
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

完整的生产 Dockerfile

# syntax=docker/dockerfile:1.4
FROM node:20-alpine AS base
RUN apk add --no-cache dumb-init
WORKDIR /app

# ── 安装依赖 ──────────────────────────────────────────
FROM base AS deps
COPY package*.json ./
RUN npm ci

# ── 构建 ─────────────────────────────────────────
FROM deps AS builder
COPY . .
RUN npm run build && npm prune --production

# ── 生产镜像 ──────────────────────────────────────
FROM base AS production

# 安全:非 root 用户
RUN addgroup -g 1001 -S nodejs && adduser -S nodeapp -u 1001

# 仅复制所需内容
COPY --from=builder --chown=nodeapp:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodeapp:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeapp:nodejs /app/package.json ./

USER nodeapp

ENV NODE_ENV=production PORT=3000
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["dumb-init", "node", "dist/index.js"]

有用的 Docker 命令

# 构建并打标签
docker build -t myapp:latest -t myapp:1.2.0 .
docker build --target production -t myapp:prod .

# 查看镜像大小分层
docker history myapp:latest

# 扫描漏洞
docker scout cves myapp:latest
# 或者
trivy image myapp:latest

# 限制资源运行
docker run \
  --memory=512m \
  --cpus=0.5 \
  --read-only \
  --tmpfs /tmp \
  -p 3000:3000 \
  myapp:latest

# 查看容器日志
docker logs -f container-name
docker logs --tail=100 container-name

# 在运行中的容器中执行命令
docker exec -it container-name sh
docker exec -it container-name node -e "console.log(process.env)"

总结

实践 影响
多阶段构建 镜像缩小 80%
Alpine 基础镜像 比完整 Node 缩小 90%
层缓存 构建速度提升 10 倍
非 root 用户 安全加固
.dockerignore 构建更快,无泄漏
健康检查 自动恢复
dumb-init 正确处理信号

→ 使用 Docker Compose 转换器 将 docker run 命令转换为 docker-compose。