Docker 最佳实践
会写 Dockerfile 和写出生产级 Dockerfile,中间差的就是这一章。前面学的都是"能跑",这里讲的是"跑得好"——镜像又小又快又安全,可观测、可复现、可回滚。把这些原则融进肌肉记忆,你的 Dockerfile 就比 90% 的人专业。
1. 多阶段构建:镜像瘦身的杀手锏
这是单个最重要的优化手段。思路很简单:编译和运行分开,最终镜像只放产物:
# 多阶段构建(multi-stage build):镜像瘦身的杀手锏
# 思路:用"构建阶段"编译代码,只把产物复制到"运行阶段"
# 最终镜像只有运行所需文件,不含编译器和源码
# === 阶段 1:构建(用完整镜像,装编译工具)===
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # 产出 dist/ 目录
# === 阶段 2:运行(用 alpine,只放产物)===
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # 只装生产依赖
COPY --from=builder /app/dist ./dist # 从构建阶段拷产物
EXPOSE 3000
CMD ["node", "dist/main.js"]
# 效果对比:
# 单阶段(node:20) : 1.1GB
# 单阶段(node:20-alpine) : 380MB
# 多阶段(alpine + 产物) : 180MB ← 瘦了 6 倍
# 通用场景:
# Go/Rust :构建用完整镜像,运行用 alpine 甚至 scratch
# Java :构建用 maven/gradle,运行用 jre-slim
# 前端 :构建用 node+webpack,运行用 nginx 静态服务原理:构建阶段用完整镜像(装编译器、源码、依赖),产出二进制或 dist 目录;运行阶段从构建阶段只 COPY 产物到精简镜像里。最终镜像不含编译器、不含源码,体积锐减。Go/Rust 这种静态编译语言效果最夸张——配上 scratch 空镜像,能做出几 MB 的镜像:
# 极端案例:Go 静态二进制 + scratch → 几 MB 的镜像
# === 构建阶段 ===
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静态编译(不依赖 glibc),关掉 CGO
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app main.go
# === 运行阶段:scratch 是空镜像(0 字节)===
FROM scratch
COPY --from=builder /app /app
# scratch 没有 CA 证书,想用 HTTPS 要手动加
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/app"]
# 最终镜像只有二进制(几 MB)+ 证书
# docker images
# myapp latest 8.5MB ← 极致多阶段构建的额外好处是安全:源码、构建工具、中间产物不进最终镜像,即使容器被攻破,攻击者也看不到源码。
2. 选对基础镜像
选型直接决定镜像大小和兼容性,牢记优先级:
- alpine(5MB 级):生产首选,极致精简。极少数依赖 musl libc 的 C 扩展可能装不上。
- slim(几十~一百多 MB):alpine 装不上包时的退路,Debian 精简版,兼容性好。
- scratch(0 字节):配合静态编译(Go/Rust)用,做出几 MB 的极致镜像。
- distroless:Google 出品,只有运行时 + 应用,没有 shell、没有包管理器,极安全。
避免用完整默认镜像(node:20、python:3.12)上生产——它们装了完整编译工具链和系统工具,几百 MB 到 1GB,既慢又增加攻击面。
3. 合并 RUN、清理缓存
每条 RUN 产生一层,层数越多镜像越大。装包后清缓存同样重要:
- 多条相关命令用
&&串联成一条 RUN。 - apt 装完用
--no-install-recommends+rm -rf /var/lib/apt/lists/*。 - alpine 用
apk add --no-cache(自动不缓存索引)。 - pip 装完用
--no-cache-dir。
具体写法见前面 Dockerfile 章的"RUN 合并"小节。这条规则配合多阶段构建,镜像能再瘦一圈。
4. .dockerignore:必配
没配就 COPY . . 是新手大忌——node_modules、.git、构建产物、.env 全进镜像。一个项目级 .dockerignore 模板:
# 完整的 .dockerignore(几乎所有项目都该有)
# 依赖目录(主机和容器架构不一致,装了反而出错)
node_modules
bower_components
vendor
# 构建产物(应该在镜像里构建,不复制主机的)
dist
build
target
*.o
*.class
# 版本控制和 IDE
.git
.gitignore
.svn
.vscode
.idea
*.swp
# 环境与密钥(绝不进镜像)
.env
.env.local
*.pem
*.key
credentials.json
secrets/
# 文档和测试(运行时不需要)
*.md
docs/
tests/
__tests__/
coverage/
.eslintcache
# Docker 自己的文件
Dockerfile
docker-compose*.yml
.dockerignore
# 日志和临时文件
*.log
tmp/
.cache/
# 系统文件
.DS_Store
Thumbs.db好处三连:加速构建(少传文件)、减小镜像(不复制无关内容)、避免泄密(密钥不进镜像)。每个项目都该有。
5. 固定版本,拒绝 latest
# 镜像标签策略:绝不只用 latest
# ❌ 错误:用 latest
FROM node:latest
docker pull myapp:latest
# 问题:
# 1. 今天和明天的 latest 可能是不同内容(无法复现)
# 2. 出问题无法回滚到上个版本
# 3. 不同环境(测试/生产)拉到的"latest"可能不一致
# ✅ 正确:固定具体版本
FROM node:20.11.0-alpine3.19
docker pull myapp:1.4.2
# 推荐的标签命名规范:
docker tag myapp yourname/myapp:1.4.2
docker tag myapp yourname/myapp:1.4.2-20240115 # 带日期
docker tag myapp yourname/myapp:1.4.2-gita1b2c3d # 带 git 短 hash
docker tag myapp yourname/myapp:1.4.2-${BUILD_ID} # CI 构建号
# 同时维护多个标签(指向同一镜像)
docker tag yourname/myapp:1.4.2 yourname/myapp:1.4 # 大版本
docker tag yourname/myapp:1.4.2 yourname/myapp:latest # 给"最新"留个入口
# 用户拉 1.4 拿到 1.4.2,拉 latest 也拿 1.4.2,但生产应固定 1.4.2这是生产事故的高频原因。某天 latest 镜像悄悄升了大版本,你的应用因为 breaking change 挂了,却找不到原因。固定具体版本(node:20.11.0-alpine3.19)让构建可复现——半年后再构建同一个 Dockerfile,产出几乎一样的镜像。
6. 安全加固
# 安全加固:让镜像"被攻破也损失最小"
# 1. 不以 root 运行(默认容器内是 root,被攻破等于拿到 root 权限)
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
USER app # 后续指令和运行时都用 app 用户
# 2. 别在镜像里放密钥(等于公开!)
# ❌ 错误:构建时用 ARG 传密钥,docker history 能看到
ARG DATABASE_PASSWORD=secret
RUN psql -c "CREATE USER app PASSWORD $DATABASE_PASSWORD"
# 上面这条指令的字符串会永久留在镜像层里,任何人 docker history 都能看到
# ✅ 正确:运行时通过环境变量 / secrets 注入
docker run -e DATABASE_PASSWORD=secret myapp
# 或用 Docker secrets(Swarm/K8s 里的安全机制)
# 3. 用 .dockerignore 排除敏感文件
# .env
# .git
# *.pem
# credentials.json
# 4. 及时更新基础镜像(修漏洞)
docker pull node:20-alpine # 拉最新 patch 版本
# 或固定具体版本 + 定期检查 CVE核心原则:假设镜像会被攻破,把损失降到最低。三条最关键:
- 非 root 运行:默认容器内是 root,被攻破等于 root 权限。加一个普通用户切换。
- 密钥绝不进镜像:
docker history能看到每层指令,ARG/RUN 里出现的密钥等于公开。运行时通过环境变量或 Docker secrets 注入。 - 最小化镜像:用 distroless 或 alpine,没有 shell、没有多余工具,攻击者拿到 shell 也无从下手。
7. 健康检查与日志
生产容器必须"可观测"——能自动检测健康、能查到日志:
# 健康检查与日志:让容器"可观测"
# HEALTHCHECK:让 Docker 自动判断容器是否健康
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost:3000/health || exit 1
# 健康状态:
# starting :启动中(start-period 内)
# healthy :健康
# unhealthy :连续 retries 次失败
# docker ps 的 STATUS 列会显示 (unhealthy)
# 配合 --restart unless-stopped 或编排工具自动重启不健康容器
# 日志策略:容器应用必须把日志写到 stdout / stderr
# ❌ 错误:写到容器内文件
const log = fs.createWriteStream('/var/log/app.log')
# ✅ 正确:console.log / process.stdout
console.log('user logged in', userId)
console.error('database error', err)
# 原因:
# 1. docker logs 能看到(否则看不到)
# 2. Docker 自动收集、轮转、可转发到 ELK/Loki
# 3. 容器删了日志不会丢(已收集到外部)
# 配置日志轮转(防磁盘爆满)
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}日志写 stdout 是容器世界的硬约定。容器是临时的,随时可能被删除/重建,日志写文件会随容器消失;写 stdout 则由 Docker 收集、轮转、转发到日志系统(ELK、Loki、CloudWatch),持久且可查询。HEALTHCHECK 让编排工具(K8s、Swarm)能判断容器是否真正"活着",而不是仅仅进程还在。
8. 镜像扫描与上线 checklist
# 生产镜像上线前 checklist
# 体积
docker images myapp # < 300MB 算合理
docker history myapp --no-trunc # 找出大层
# 安全
docker scout cves myapp # 官方漏洞扫描
trivy image myapp # 第三方扫描工具
# 任何 HIGH/CRITICAL 漏洞都要处理
# 配置
docker run --rm myapp whoami # 应该不是 root
docker run --rm myapp env | grep PASSWORD # 应该没有密钥
docker inspect myapp | grep -i healthcheck # 应该有健康检查
# 性能
docker run --rm -it --memory=256m myapp # 限制内存能跑
# 多次启动耗时 < 5s(容器应该轻量快速)
# 可重现
docker build --no-cache -t myapp:repro . # 无缓存构建成功
docker run --rm myapp:repro --version # 版本号正确CI/CD 流水线里应该自动跑漏洞扫描,任何 HIGH/CRITICAL 漏洞阻断部署。Docker 官方的 docker scout 和开源的 trivy 都不错。基础镜像也要定期更新——新 CVE 每月都出,半年前的 alpine 可能已经有一堆已知漏洞。
9. 缓存友好的 Dockerfile(回顾)
构建速度也是生产力的关键。让 Dockerfile缓存友好的三条原则(详见 Dockerfile 章):
- 不变的指令放前面(基础镜像、装系统库、装依赖)。
- 常变的放后面(复制源码、构建)。
- 先
COPY package*.json再RUN npm install,最后COPY . .。
这样改一行代码,前面所有层都命中缓存,构建从几分钟变几秒。BuildKit(DOCKER_BUILDKIT=1)还能做更聪明的缓存(跨机器共享缓存层),CI 加速效果显著。
小结
把本系列 10 篇串起来,Docker 的完整学习路径是:
- 入门:理解镜像/容器概念,会用 pull/run 基础命令。
- 工程化:写 Dockerfile 把自己项目打包,管理 Volume 和网络。
- 编排:用 Compose 管理多服务,推到 Registry 分发。
- 优化:多阶段构建、安全加固、可观测,做出生产级镜像。
掌握 Docker,你就拥有了"环境一致"的超能力——开发、测试、生产跑的是同一份镜像,"在我电脑上能跑"不再是借口。它是通往云原生、DevOps、Kubernetes 世界的第一张门票。下一篇该学什么?Kubernetes(容器编排)、Helm(K8s 包管理)、ArgoCD(GitOps)都在等你。
← 上一篇 Docker 镜像仓库
返回 Docker 教程目录