Dockerfile最佳实践与多阶段构建
1. 指令速查
| 指令 | 用途 |
|---|---|
FROM | 基础镜像 |
WORKDIR | 切目录(自动创建) |
COPY / ADD | 复制文件 |
RUN | 构建时执行命令(生成新层) |
CMD | 默认启动命令(可被 docker run 覆盖) |
ENTRYPOINT | 入口,CMD 当参数 |
ENV | 环境变量 |
ARG | 构建参数(不进运行时镜像) |
EXPOSE | 文档化端口(不实际开放) |
VOLUME | 声明卷 |
USER | 切用户 |
HEALTHCHECK | 健康检查 |
COPY 优于 ADD(ADD 有自动解压、远程下载等隐式行为)。
2. 多阶段构建(最重要)
前端构建产物只是几 MB 静态文件,运行时不需要 node_modules、构建工具。
2.1 SPA 构建 + Nginx 服务
# 阶段 1:构建
FROM node:20-alpine AS builder
WORKDIR /app
# 单独 copy package.json 让 npm install 层可缓存
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2:运行(只复制产物)
FROM nginx: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;"]
最终镜像大小:~25MB(vs 全装 Node 1GB+)。
2.2 Node SSR(Next.js)
# 阶段 1:依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 阶段 2:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# 阶段 3:运行
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S -u 1001 -G nodejs nextjs
# Next.js standalone 模式:构建产物含最小 node_modules
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.js 加 output: 'standalone' 启用独立模式。
3. 层缓存优化
每个 RUN、COPY 是一个 layer。Docker 检查 layer 是否可复用:
COPY/ADD:看文件内容 hashRUN:看命令字符串
核心原则:不变的放前面,多变的放后面。
# ✗ 错:代码变就重装依赖
COPY . .
RUN npm install
# ✓ 对:依赖变才重装
COPY package*.json ./
RUN npm ci
COPY . .
npm ci 优于 npm install:
- 严格按 lockfile 安装
- 删除已存在的 node_modules
- CI 场景快很多
4. .dockerignore
减少 build context 大小、避免泄漏:
node_modules
.git
.env
.env.*
!.env.example
*.log
dist
build
.next
.cache
.DS_Store
README.md
.vscode
.idea
coverage
.github
复制 .git 进镜像不仅大,可能含敏感历史。
5. ARG 与多环境
ARG NODE_ENV=production
ARG API_URL
ENV NODE_ENV=${NODE_ENV}
ENV NEXT_PUBLIC_API_URL=${API_URL}
RUN npm run build
docker build --build-arg API_URL=https://api.example.com -t myapp .
注意:ARG 仅构建时存在,不进最终镜像。但敏感信息不要用 ARG:docker history 能看到(虽然 BuildKit 已改进)。
5.1 BuildKit secret(推荐)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .
secret 不会进任何 layer。
6. CMD vs ENTRYPOINT
# 模式 1:只 CMD
CMD ["node", "server.js"]
# docker run myimage → node server.js
# docker run myimage bash → bash(覆盖)
# 模式 2:ENTRYPOINT + CMD
ENTRYPOINT ["node"]
CMD ["server.js"]
# docker run myimage → node server.js
# docker run myimage app.js → node app.js(CMD 被替换)
# 模式 3:tini wrapper
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
用 exec 形式(JSON 数组),不用 shell 形式:
# ✗ shell 形式:实际是 /bin/sh -c "node server.js",多一层 PID 1,信号处理坏
CMD node server.js
# ✓ exec 形式:node 直接是 PID 1(或 tini 子进程)
CMD ["node", "server.js"]
7. HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
K8s 一般用自己的 readinessProbe / livenessProbe,会忽略 Dockerfile HEALTHCHECK。本地 docker / docker-compose 用得上。
8. 安全基线
8.1 非 root 运行
# Node 官方镜像内置 node 用户(uid 1000)
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
任何容器都不应以 root 跑应用。
8.2 锁定基础镜像
# ✗ 用 latest,构建结果不可重现
FROM node:latest
# ✓ 锁版本,更好用 digest
FROM node:20.11.1-alpine3.19
FROM node@sha256:abc123...
CI 里定期更新基础镜像 digest。
8.3 最小化攻击面
- 用 alpine / distroless / slim
- 不装调试工具(curl、wget、bash)到运行时镜像
- 删除 apt cache:
rm -rf /var/lib/apt/lists/* - 删除 npm cache:
npm cache clean --force(npm ci 不需要)
8.4 不写敏感信息到镜像
# ✗ 永远不要
ENV DB_PASSWORD=mypassword
# ✓ 运行时注入
# docker run -e DB_PASSWORD=xxx
# 或用 K8s Secret / Vault
9. 完整生产示例
# syntax=docker/dockerfile:1
ARG NODE_VERSION=20.11.1
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
FROM node:${NODE_VERSION}-alpine AS runner
RUN apk add --no-cache tini
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --from=builder --chown=node:node /app/package.json ./
USER node
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
10. 常见反模式
- 每次 COPY . .:代码变重装依赖
RUN apt-get install不删 cache:层膨胀USER root跑应用:被入侵直接宿主 rootFROM ubuntu:latest:构建不可重现 + 镜像 1GB+- shell 形式 CMD:信号处理坏
- secret 写 ENV:
docker history泄露 - 没 .dockerignore:build context 几 GB
- 多个软件装一个镜像:违反单一职责,不能独立扩缩容
- 不指定 USER 让用户自己改:默认 root 风险