# Dockerfile 编写指南 > Dockerfile 是**镜像构建的蓝图**,每一行指令都会在镜像中生成一层。写好 Dockerfile 的核心目标:**镜像更小、构建更快、启动更安全**。 --- ## 一、Dockerfile 基本结构 ```dockerfile # 1. 基础镜像 FROM node:20-slim AS builder # 2. 维护者信息(可选) LABEL maintainer="yourname@example.com" # 3. 设置工作目录 WORKDIR /app # 4. 复制依赖文件并安装 COPY package*.json ./ RUN npm ci --production # 5. 复制源代码 COPY . . # 6. 构建(开发环境需要,生产环境不需要) RUN npm run build # 7. 暴露端口 EXPOSE 3000 # 8. 启动命令 CMD ["node", "dist/index.js"] ``` --- ## 二、常用指令详解 ### 2.1 `FROM` — 基础镜像 ```dockerfile # 选镜像的通用原则: # 有 Alpine 选 Alpine(体积小) # 有 Slim 选 Slim(平衡体积和功能) # 没有再选完整版本 FROM node:20-alpine # 推荐:最小体积 FROM node:20-slim # 推荐:平衡方案 FROM node:20 # 不推荐:包含完整 OS,镜像臃肿 FROM python:3.12-slim-bookworm FROM nginx:alpine FROM golang:1.22-alpine FROM ubuntu:24.04 # 一般作为多阶段构建的第一步 ``` **常见基础镜像体积对比:** | 镜像 | 大小 | 适用场景 | |------|------|----------| | `alpine` | 5-10MB | 生产环境、追求极致体积 | | `slim` | 100-200MB | 大多数生产场景,兼容性好 | | 完整版 | 1GB+ | 开发环境、需要完整工具链 | ### 2.2 `RUN` — 执行命令 ```dockerfile # 合并 RUN 指令减少镜像层数 # 不推荐:每行一个 RUN,产生多层 RUN apt-get update RUN apt-get install -y nginx RUN rm -rf /var/lib/apt/lists/* # 推荐:合并为一行,并用 && 连接,用 \ 换行 RUN apt-get update && \ apt-get install -y nginx && \ rm -rf /var/lib/apt/lists/* # 注意:RUN 产生的层会**永久留在镜像中**,即使后续 rm 了也没用 # 所以更新和安装、清理要放在同一个 RUN 里 ``` ### 2.3 `COPY` vs `ADD` ```dockerfile # COPY — 简单复制,推荐优先使用 COPY package.json ./ COPY . . # ADD — 有额外功能但不可预测,不推荐 ADD https://example.com/file.tar.gz /tmp/ # 能下载远程文件,但容易出错 ADD file.tar.gz /app/ # 能自动解压,但 COPY + RUN tar 更清晰 # 结论:99% 的情况用 COPY,ADD 只在自动解压 tar 和下载远程文件时有用 ``` ### 2.4 `WORKDIR` — 工作目录 ```dockerfile WORKDIR /app # 设置工作目录 COPY package.json ./ # 相对于 /app RUN npm install COPY . . # 复制代码到 /app # WORKDIR 会被后续指令继承,且会自动创建(不需要提前 mkdir) WORKDIR /subdir # 现在是 /app/subdir RUN pwd # 输出 /app/subdir ``` ### 2.5 `EXPOSE` — 声明端口 ```dockerfile # 声明容器运行时监听的端口(文档性质,不自动映射) EXPOSE 3000 EXPOSE 80 443 # 实际端口映射需要在 docker run -p 或 docker-compose 中指定 ``` ### 2.6 `ENV` — 环境变量 ```dockerfile ENV NODE_ENV=production ENV APP_PORT=3000 ENV DB_HOST=localhost # 容器启动时可以覆盖: # docker run -e DB_HOST=192.168.1.100 my-app ``` ### 2.7 `CMD` vs `ENTRYPOINT` ```dockerfile # CMD — 容器启动时执行的默认命令(可被覆盖) CMD ["node", "dist/index.js"] # 运行 docker run my-app echo "hello" 会覆盖 CMD # ENTRYPOINT — 入口点,不可被 docker run 覆盖(但可用 --entrypoint 强制覆盖) ENTRYPOINT ["node"] CMD ["dist/index.js"] # 作为 ENTRYPOINT 的参数 # 实际开发中,大多数情况用 CMD 就够了 ``` --- ## 三、多阶段构建(关键优化手段) ### 3.1 为什么需要多阶段构建 前端项目用 Next.js/Vite + TypeScript 构建,需要 Node.js 全套环境。但运行只需要一个编译后的静态文件。多阶段构建可以**把构建工具留在第一阶段,最终镜像只放产物**。 ### 3.2 前端多阶段构建(Node + TypeScript) ```dockerfile # ========== 第一阶段:构建 ========== FROM node:20-alpine AS builder WORKDIR /app # ① 先复制依赖文件,利用 Docker 缓存 COPY package*.json ./ RUN npm ci # ② 再复制源码(代码改了才重新构建) COPY . . # ③ 构建 RUN npm run build # ========== 第二阶段:运行 ========== FROM node:20-alpine AS production # 安装 tini(处理僵尸进程,可选但推荐) RUN apk add --no-cache tini WORKDIR /app # 仅复制 production 依赖 COPY package*.json ./ RUN npm ci --production # 从构建阶段复制产物 COPY --from=builder /app/dist ./dist COPY --from=builder /app/public ./public ENV NODE_ENV=production EXPOSE 3000 # 使用 tini 作为 PID 1 ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "dist/index.js"] ``` **核心思路:先用大镜像构建 → 再把产物复制到最小镜像中运行。** ### 3.3 Java 多阶段构建 ```dockerfile # 第一阶段:Maven 构建 FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn package -DskipTests # 第二阶段:运行 FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ``` ### 3.4 Go 多阶段构建 ```dockerfile # 第一阶段:编译 FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app/server # 第二阶段:运行 FROM alpine:3.19 RUN apk --no-cache add ca-certificates COPY --from=builder /app/server /app/server EXPOSE 8080 CMD ["/app/server"] ``` --- ## 四、.dockerignore 文件 > **和 `.gitignore` 一样重要!** 不写 `.dockerignore` 会导致不必要的文件被复制到镜像中,增大体积、暴露敏感信息。 ```dockerignore # 依赖 node_modules npm-debug.log # Git .git .gitignore # 构建产物(别把 build 直接拷进去,让容器自己 build) dist build *.local # 编辑器 .vscode .idea *.swp # Docker 自身 docker-compose.yml Dockerfile .dockerignore # 文档和测试 README.md LICENSE tests __tests__ # 环境变量(千万不要拷进去!) .env .env.local .env.*.local ``` --- ## 五、最佳实践清单 ### ✅ 应该做 | 实践 | 说明 | |------|------| | 使用具体版本号 | `FROM node:20.11-alpine`,不要用 `latest` | | 用 `alpine` 或 `slim` | 减小镜像体积 | | 合并 `RUN` 指令 | 减少镜像层数 | | 先复制 `package.json` 再 `npm install` | 利用 Docker 层缓存 | | 用 `.dockerignore` | 排除不必要的文件 | | 用多阶段构建 | 分离构建和运行环境 | | 非 root 用户运行 | 安全考虑 | | 用 `CMD ["exec格式"]` | 避免 shell 包装问题 | ### ❌ 不应该做 | 实践 | 问题 | |------|------| | 用 `latest` 标签 | 构建结果不可复现 | | 在 Dockerfile 中存密码/密钥 | 暴露敏感信息 | | 用 `ADD` 替代 `COPY` | 不可预测行为 | | 安装不必要的应用 | 镜像臃肿 | | 用 `RUN apt-get install && apt-get remove` | 删除的文件仍在镜像层中 | | 以 root 用户运行应用 | 安全风险 | --- ## 六、非 Root 用户示例 ```dockerfile FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . # 创建非 root 用户 RUN addgroup -S appgroup && \ adduser -S appuser -G appgroup # 切换用户 USER appuser EXPOSE 3000 CMD ["node", "dist/index.js"] ``` --- ## 七、常见问题排查 ### 7.1 镜像太大 ```bash # 分析镜像各层大小 docker history my-app:latest # 检查是否有未清理的缓存 # 解决:使用 alpine/slim 基础镜像 + 多阶段构建 + .dockerignore ``` ### 7.2 构建缓存失效 ```dockerfile # 错误:代码改动导致 npm install 也重新执行 COPY . . RUN npm install # 正确:依赖文件不变时跳过 npm install COPY package*.json ./ RUN npm install COPY . . ``` ### 7.3 容器启动后立即退出 ```bash # 查看日志 docker logs <容器名> # 常见原因: # 1. CMD/ENTRYPOINT 写错了 # 2. 主进程不是 PID 1(用 tini 或 dumb-init 处理) # 3. 应用启动失败(检查日志) ``` ### 7.4 端口无法访问 ```bash # 1. 检查 docker run -p 是否映射 docker run -p 3000:3000 ... # 2. 检查容器内应用是否监听 0.0.0.0(不是 localhost) # 3. 检查防火墙 # 4. 用 docker-compose 时,确认 services 定义正确 ``` --- ## 七、Docker 安全加固 ### 7.1 使用非 root 用户 ```dockerfile # 最简方式(Node 官方镜像已内置 node 用户) FROM node:20-alpine USER node CMD ["node", "server.js"] # 手动创建用户(通用模式) FROM node:20-alpine WORKDIR /app COPY --chown=node:node package*.json ./ RUN npm ci COPY --chown=node:node . . USER node CMD ["node", "dist/index.js"] ``` > `--chown=user:group` 在 COPY/RUN 时设置文件所有者,**避免 root 拥有文件**后切换到非 root 用户导致权限问题。 ### 7.2 不要安装 build 依赖到生产镜像 ```dockerfile # 错误:所有依赖都装进去了 FROM node:20-alpine RUN npm install # devDependencies 也被安装了 # 正确:生产环境只装 dependencies RUN npm ci --only=production # 更正确:多阶段构建 FROM node:20-alpine AS builder RUN npm install # 包含 devDependencies RUN npm run build FROM node:20-alpine RUN npm ci --only=production COPY --from=builder /app/dist ./dist ``` ### 7.3 扫描镜像漏洞 ```bash # 使用 trivy(推荐) docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image nginx:latest # 扫描指定镜像 trivy image my-app:v1.0 # 只输出高危及以上 trivy image --severity HIGH,CRITICAL my-app:v1.0 # 扫描 Dockerfile 本身(不用构建镜像) trivy config Dockerfile ``` ### 7.4 密钥管理原则 ```dockerfile # ❌ 绝对不要硬编码密码 RUN echo "password=123456" > /app/config.env # ❌ 不要在构建时传递密钥(会留在镜像层中) docker build --build-arg DB_PASSWORD=secret . # ✅ 运行时传入环境变量 docker run -e DB_PASSWORD=secret my-app # ✅ 或使用 Docker secrets(Swarm 模式) # ✅ 生产环境用 Kubernetes Secrets / AWS Secrets Manager ``` --- ## 八、实战:Node.js + Express 完整 Dockerfile ```dockerfile # Build stage FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . # Production stage FROM node:20-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json ENV NODE_ENV=production ENV PORT=3000 RUN addgroup -S app && adduser -S app -G app USER app EXPOSE 3000 CMD ["node", "dist/server.js"] ``` --- > 下一步:理解 Dockerfile 后,学习 [[docker-compose]] 来编排多容器应用。