--- tags: - docker - devops - containerization create time: 2026-04-28 14:30 --- # Dockerfile 编写指南 > 一份从"能跑"到"跑得好"的 Dockerfile 教程。通过教学式讲解、流程图和实战示例,帮你写出体积小、构建快、启动安全的镜像。 ## 概述 Dockerfile 是镜像构建的蓝图,每一行指令都会在镜像中生成一层。写好 Dockerfile 的核心目标:**镜像更小、构建更快、启动更安全**。 > [!tip] 初学者思考题 > 为什么 `RUN apt-get install nginx && rm -rf /var/lib/apt/lists/*` 不能拆成两个 RUN?拆开后镜像体积会不会变小?(答案见正文 2.2 节) ## 正文 ### 一、Dockerfile 基本结构 一张图理解 Dockerfile 的构建流程: ```mermaid flowchart LR A["FROM 基础镜像"] --> B["WORKDIR 设工作目录"] B --> C["COPY 依赖文件 + RUN 安装"] C --> D["COPY 源代码"] D --> E["RUN 构建产物"] E --> F["EXPOSE 声明端口"] F --> G["CMD 启动命令"] style A fill:#e1f5fe style G fill:#c8e6c9 ``` > [!note] 构建流程 > 构建过程是**自上而下逐行执行**的,每一步都基于上一步的结果生成新层。层序排列直接影响缓存命中率和镜像体积。 一个标准的 Node.js 项目 Dockerfile: ```dockerfile # 1. 基础镜像 FROM node:20-slim AS builder # 2. 维护者信息(可选) LABEL maintainer="yourname@example.com" # 3. 设置工作目录 WORKDIR /app # 4. 先复制依赖文件,利用 Docker 缓存 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` — 基础镜像 > [!question] 为什么镜像大小差距这么大? > 完整 OS + 工具链 vs 精简版 = 1GB vs 10MB,这就是 `FROM` 选择的影响。 **选镜像原则:有 Alpine 选 Alpine,有 Slim 选 Slim,没有再选完整版。** ```dockerfile FROM node:20-alpine # 推荐:最小体积 ~150MB FROM node:20-slim # 推荐:平衡方案 ~200MB FROM node:20 # 不推荐:完整 OS,镜像臃肿 ~1GB FROM python:3.12-slim-bookworm FROM golang:1.22-alpine FROM ubuntu:24.04 # 一般作为多阶段构建的第一步 ``` | 镜像类型 | 大小 | 适用场景 | |----------|------|----------| | `alpine` | 5-10MB | 生产环境、追求极致体积 | | `slim` | 100-200MB | 大多数生产场景,兼容性好 | | 完整版 | 1GB+ | 开发环境、需要完整工具链 | #### 2.2 `RUN` — 执行命令 > [!warning] 关键概念:Docker 层不可删 > `RUN` 产生的层会**永久留在镜像中**,即使后续 `rm` 了也没用。所以更新、安装、清理必须在**同一个 RUN** 里。 ```dockerfile # ❌ 不推荐:每行一个 RUN,产生多层,rm 无效 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/* ``` #### 2.3 `COPY` vs `ADD` > [!abstract] 简单原则 > **99% 的情况用 COPY。** ADD 的额外功能(自动解压、远程下载)行为不可预测。 ```dockerfile # COPY — 简单复制,优先使用 COPY package.json ./ COPY . . # ADD — 仅在两种场景有用 ADD https://example.com/file.tar.gz /tmp/ # 下载远程文件(但容易出错) ADD file.tar.gz /app/ # 自动解压(COPY + RUN tar 更清晰) ``` #### 2.4 `WORKDIR` — 工作目录 ```dockerfile WORKDIR /app # 设置工作目录 COPY package.json ./ # 相对于 /app RUN npm install COPY . . # WORKDIR 会被后续指令继承,且会自动创建 WORKDIR /subdir # 现在是 /app/subdir RUN pwd # 输出 /app/subdir ``` #### 2.5 `EXPOSE` — 声明端口 > [!info] EXPOSE 不自动映射端口 > `EXPOSE` 只是**文档性质**的声明,实际端口映射需要在 `docker run -p` 或 `docker-compose` 中指定。 ```dockerfile EXPOSE 3000 EXPOSE 80 443 ``` #### 2.6 `ENV` — 环境变量 ```dockerfile ENV NODE_ENV=production ENV APP_PORT=3000 # 容器启动时可以覆盖:docker run -e DB_HOST=192.168.1.100 my-app ``` #### 2.7 `CMD` vs `ENTRYPOINT` | 指令 | 特点 | 可被 docker run 覆盖? | |------|------|----------------------| | `CMD` | 默认命令,最灵活 | ✅ 完全覆盖 | | `ENTRYPOINT` | 入口点,固定执行 | ⚠️ 需用 `--entrypoint` 强制覆盖 | ```dockerfile # CMD — 大多数情况用这个就够了 CMD ["node", "dist/index.js"] # ENTRYPOINT + CMD — 入口固定,参数可配 ENTRYPOINT ["node"] CMD ["dist/index.js"] ``` --- ### 三、多阶段构建(核心优化手段) > [!abstract] 一句话理解多阶段构建 > **先用大镜像构建 → 再把产物复制到最小镜像中运行。** 构建工具不留到最终镜像。 构建流程图: ```mermaid flowchart TB subgraph Phase1["第一阶段:构建(大镜像)"] A["node:20-alpine"] --> B["安装全套依赖 + devDependencies"] B --> C["TypeScript / Maven / Go 编译"] C --> D["生成 dist/ / target/*.jar"] end subgraph Phase2["第二阶段:运行(最小镜像)"] E["node:20-alpine / JRE Alpine / Alpine"] --> F["只装 production 依赖"] F --> G["COPY --from=builder 复制产物"] G --> H["启动应用"] end Phase1 -->|COPY --from=builder| Phase2 style Phase1 fill:#fff3e0 style Phase2 fill:#e8f5e9 ``` #### 3.1 前端多阶段构建(Node + TypeScript) ```dockerfile # ========== 第一阶段:构建 ========== FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci # ① 先装依赖,利用缓存 COPY . . # ② 再复制源码,代码改了才重建 RUN npm run build # ③ 编译 # ========== 第二阶段:运行 ========== FROM node:20-alpine AS production RUN apk add --no-cache tini # tini 处理僵尸进程(推荐) WORKDIR /app COPY package*.json ./ RUN npm ci --production # 只装 production 依赖 COPY --from=builder /app/dist ./dist COPY --from=builder /app/public ./public ENV NODE_ENV=production EXPOSE 3000 ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "dist/index.js"] ``` #### 3.2 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 # 第二阶段:运行(JRE 镜像,无 Maven) FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] ``` #### 3.3 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 # 第二阶段:运行(纯 Alpine,仅几 MB) FROM alpine:3.19 RUN apk --no-cache add ca-certificates COPY --from=builder /app/server /app/server EXPOSE 8080 CMD ["/app/server"] ``` --- ### 四、.dockerignore 文件 > [!danger] 和 .gitignore 一样重要! > 不写 `.dockerignore` 会把不必要的文件塞进镜像,增大体积、暴露密钥。 ```dockerignore # 依赖 node_modules npm-debug.log # Git .git .gitignore # 构建产物(别把 build 拷进去,让容器自己 build) dist build *.local # 编辑器和文档 .vscode .idea *.swp README.md LICENSE tests # Docker 自身文件 docker-compose.yml Dockerfile .dockerignore # 环境变量(千万不要拷进去!) .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 用户运行应用 | 安全风险 | --- ### 六、安全加固 #### 6.1 使用非 root 用户 ```dockerfile FROM node:20-alpine WORKDIR /app # 利用 --chown 设置文件所有者,避免切换用户后权限问题 COPY --chown=node:node package*.json ./ RUN npm ci COPY --chown=node:node . . # 切换到非 root 用户 USER node EXPOSE 3000 CMD ["node", "dist/index.js"] ``` #### 6.2 不要安装 build 依赖到生产镜像 ```dockerfile # ❌ 错误:devDependencies 也被装进镜像 RUN npm install # ✅ 正确:多阶段构建 FROM node:20-alpine AS builder RUN npm install # 包含 devDependencies RUN npm run build FROM node:20-alpine RUN npm ci --production # 只装 production 依赖 COPY --from=builder /app/dist ./dist ``` #### 6.3 密钥管理原则 ```dockerfile # ❌ 绝对不要硬编码密码 RUN echo "password=123456" > /app/config.env # ❌ 不要在构建时传递密钥(会留在镜像层中) # docker build --build-arg DB_PASSWORD=secret . # ✅ 运行时传入环境变量 # docker run -e DB_PASSWORD=secret my-app # ✅ 生产环境用 Kubernetes Secrets / AWS Secrets Manager ``` #### 6.4 扫描镜像漏洞 ```bash # 使用 trivy 扫描镜像 trivy image my-app:v1.0 # 只输出高危及以上 trivy image --severity HIGH,CRITICAL my-app:v1.0 # 直接扫描 Dockerfile(不用构建镜像) trivy config Dockerfile ``` --- ### 七、构建缓存优化 > [!tip] 缓存命中规则 > Docker 从上到下逐层检查缓存。**任何一行变更,后续所有层全部失效重构建。** 所以要把经常变的内容放下面。 ```dockerfile # ❌ 错误:代码改动 → npm install 也重新执行 COPY . . RUN npm install # ✅ 正确:先复制不变的依赖文件 COPY package*.json ./ RUN npm install COPY . . ``` 构建缓存策略流程图: ```mermaid flowchart LR A["package.json 未变"] -->|"缓存命中"| B["跳过 npm install"] C["package.json 变更"] -->|"缓存失效"| D["重新 npm install"] E["源码变更"] --> B B --> F["最终镜像"] D --> F style A fill:#c8e6c9 style C fill:#fff3e0 style E fill:#fff3e0 ``` --- ### 八、常见问题排查 #### 8.1 镜像太大 ```bash # 分析镜像各层大小 docker history my-app:latest # 解决:alpine 基础镜像 + 多阶段构建 + .dockerignore ``` #### 8.2 容器启动后立即退出 ```bash # 查看日志 docker logs <容器名> # 常见原因: # 1. CMD/ENTRYPOINT 写错了 # 2. 主进程不是 PID 1(用 tini 或 dumb-init) # 3. 应用启动失败(检查日志) ``` #### 8.3 端口无法访问 ```bash # 1. 检查 docker run -p 是否映射 docker run -p 3000:3000 ... # 2. 容器内应用监听 0.0.0.0(不是 localhost) # 3. 检查防火墙 ``` --- ## 关联笔记 - [[docker-compose]] — 用 Compose 编排多容器应用 - [[Docker常用命令]] — 日常 Docker 操作速查