跳到主要内容

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.jsoutput: 'standalone' 启用独立模式。

3. 层缓存优化

每个 RUNCOPY 是一个 layer。Docker 检查 layer 是否可复用:

  • COPY / ADD:看文件内容 hash
  • RUN:看命令字符串

核心原则:不变的放前面,多变的放后面

# ✗ 错:代码变就重装依赖
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 仅构建时存在,不进最终镜像。但敏感信息不要用 ARGdocker 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 跑应用:被入侵直接宿主 root
  • FROM ubuntu:latest:构建不可重现 + 镜像 1GB+
  • shell 形式 CMD:信号处理坏
  • secret 写 ENVdocker history 泄露
  • 没 .dockerignore:build context 几 GB
  • 多个软件装一个镜像:违反单一职责,不能独立扩缩容
  • 不指定 USER 让用户自己改:默认 root 风险

11. 延伸阅读