This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/gyh/docker/Dockerfile编写指南.md
T

476 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 操作速查