
Node.js 应用的 Docker 最佳实践:生产就绪容器
将 Node.js 应用容器化看似简单——直到你遇到镜像体积过大、安全漏洞或构建缓慢等问题。这些最佳实践将帮助你构建快速、小巧且安全的 Docker 镜像。
基础 Dockerfile(及其问题)
# ❌ 简单粗暴的方法——不要在生产中使用
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "src/index.js"]
问题:
- 镜像超过 1GB(完整 Node.js + 开发依赖)
- 从主机复制 node_modules(或每次都重新构建)
- 以 root 用户运行(安全风险)
- 没有层缓存优化
- 生产镜像中包含开发依赖

最佳实践 #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

最佳实践 #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 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。