一、学习目标

  1. 掌握 Dockerfile 编写最佳实践(合并 RUN、利用缓存、减小层数)

  2. 学会镜像瘦身技巧(多阶段构建、Alpine 基础镜像)

  3. 了解安全实践(避免硬编码秘密、使用 ARG、非 root 用户)


二、Dockerfile 最佳实践

1. 合并 RUN 指令,减少层数

每个 RUN、COPY、ADD 指令都会在镜像中创建一个新层。层数越多,镜像体积越大,构建时间也可能变长(虽然层可缓存)。

❌ 不好的写法:

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y nginx
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

✅ 好的写法:

FROM ubuntu:22.04
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        nginx \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

关键点:

  • 使用 && 连接命令,确保失败时构建失败

  • 安装后立即清理缓存(apt-get clean、rm -rf /var/lib/apt/lists/*)

  • 使用 --no-install-recommends 避免安装不必要的推荐包

2. 利用构建缓存

Docker 会逐条执行指令,并检查缓存是否可用。缓存命中条件:当前指令与之前构建时的指令完全相同,且其依赖的所有文件(如 COPY 的文件)未改变。

策略:将不易变化的指令放在前面,经常变化的代码放在后面。

# ✅ 高效利用缓存
FROM node:18-alpine

# 先复制 package.json(变化较少)
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production   # 依赖安装层会被缓存,除非 package.json 改变

# 后复制源代码(经常变化)
COPY . .

CMD ["node", "server.js"]

反之:如果先复制所有代码,每次修改代码都会导致 npm install 重新执行,浪费构建时间。

3. 使用 .dockerignore 排除无关文件

类似 .gitignore,可以排除本地开发文件、版本控制目录、日志等,避免它们进入构建上下文和镜像。

.dockerignore 示例:

# Git
.git
.gitignore

# 依赖
node_modules
__pycache__
*.pyc
.venv

# 开发工具
.vscode
.idea
*.log

# 环境变量
.env
.env.local

# 文档
README.md
docs/

4. 固定基础镜像标签

不要使用 latest,因为 latest 指向的版本会变化,导致镜像不可复现。

# ❌ 不好
FROM python:latest

# ✅ 好
FROM python:3.9.18-slim

5. 按需安装包,清理临时文件

RUN apt-get update && apt-get install -y --no-install-recommends \
        build-essential \
        curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && rm -rf /tmp/* /var/tmp/*

三、镜像瘦身技巧

1. 选择轻量级基础镜像

镜像类型大小(大约)适用场景
ubuntu:22.0477MB需要完整 Linux 环境
debian:stable-slim45MB兼容性好,比 Ubuntu 小
alpine:3.187MB极小体积,使用 musl libc
python:3.9-alpine45MBPython 应用
node:18-alpine50MBNode.js 应用
golang:1.21-alpine80MBGo 编译环境(但可用 scratch 更小)

注意事项:

  • Alpine 使用 apk 包管理器,软件包可能不如 apt 丰富

  • 某些 Python 库需要编译(如 numpy),在 Alpine 上可能遇到 musl libc 兼容问题,此时可选择 slim 版本

2. 多阶段构建(Multi-stage Builds)

核心思想:在 Dockerfile 中使用多个 FROM 语句,每个阶段可以基于不同的镜像。前阶段用于编译/构建,后阶段只复制必要的产物,从而大幅减小最终镜像体积。

示例1:Go 应用

# 阶段1:构建(使用完整 Go 镜像)
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp -ldflags="-s -w" .   # -s -w 去除调试信息

# 阶段2:运行(使用最精简的 alpine 或 scratch)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

示例2:前端应用(React/Vue)

# 阶段1:构建静态文件
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build   # 生成 dist 目录

# 阶段2:使用 nginx 提供静态服务
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

示例3:Python 应用(需要编译依赖)

# 阶段1:编译依赖(使用完整工具链)
FROM python:3.9-slim AS builder
WORKDIR /install
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# 阶段2:运行(只复制已安装的包,无需编译工具)
FROM python:3.9-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
CMD ["python", "app.py"]

3. 清理不必要的文件

# 对于 apt
RUN apt-get update && apt-get install -y curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 对于 pip
RUN pip install --no-cache-dir -r requirements.txt

# 对于 npm
RUN npm ci --only=production && npm cache clean --force

4. 使用 --link 选项(BuildKit)优化复制

从 Docker 18.09+ 开始,启用 BuildKit 后可以使用 --link 模式复制,减少层大小(较高级,按需了解)。


四、安全实践

1. 避免硬编码密码/密钥

❌ 绝对不要这样做:

ENV DB_PASSWORD=123456
RUN echo "secret=abc123" > config.ini

✅ 好的做法:

  • 使用构建参数 ARG(仅在构建时可用,不会持久化到镜像?实际上 ARG 值会留在镜像历史中!)

  • 使用环境变量 ENV 在运行时通过 -e 传入

  • 使用 Docker secrets(Swarm)或 Kubernetes secrets

  • 使用外部密钥管理服务(Vault、AWS Secrets Manager)

注意:ARG 和 ENV 的值都可以通过 docker history 看到,所以不要用来传递敏感信息!真正的秘密应该在运行时注入。

# 构建参数(值可通过 --build-arg 传入,但会留在镜像历史中)
ARG DB_PASSWORD
RUN echo "password is $DB_PASSWORD"   # 危险!会在镜像层中留下明文

# 更好的方式:运行时传入环境变量
ENV DB_PASSWORD=${DB_PASSWORD}   # 运行时指定

运行时注入秘密:

# 使用环境变量(不够安全,因为 env 可在容器内看到)
docker run -e DB_PASSWORD=secret myapp

# 使用文件挂载(稍好)
echo "secret" > /tmp/db-pass
docker run -v /tmp/db-pass:/run/secrets/db-pass myapp

# 使用 Docker secrets(Swarm 模式)
printf "secret" | docker secret create db_pass -

2. 使用非 root 用户运行应用

默认容器以 root 用户运行,这违反了最小权限原则。应该创建普通用户并切换到该用户。

FROM node:18-alpine

# 创建用户和组(-S 表示系统用户)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .

USER appuser

CMD ["node", "server.js"]

对于不同基础镜像:

# Debian/Ubuntu 系列
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser

# Alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 指定 UID/GID 保持一致性
RUN adduser -u 1000 -D appuser

3. 使用 COPY --chown 确保文件所有权

COPY --chown=appuser:appgroup . /app

4. 固定依赖版本,定期扫描漏洞

  • 在 requirements.txt、package.json 中固定版本号

  • 使用 docker scan 或第三方工具(Trivy、Clair)扫描镜像漏洞

  • 定期更新基础镜像

docker scan myimage:latest

5. 不要安装不必要的软件包

# ❌ 不好:安装编译工具但不需要
RUN apt-get install -y gcc make

# ✅ 好:只在构建阶段使用编译工具,最后不保留

6. 使用 --no-cache-dir 或类似选项

RUN pip install --no-cache-dir -r requirements.txt
RUN npm install --no-optional && npm cache clean --force
RUN apk add --no-cache curl   # Alpine 的 --no-cache 不保存本地缓存索引

7. 限制资源使用(docker run 时)

虽然不是 Dockerfile 内容,但运行时也应设置资源限制:

docker run --memory="512m" --cpus="1.0" --security-opt=no-new-privileges myapp

五、综合优化示例

优化前(典型新手 Dockerfile)

FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y python3 python3-pip
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
EXPOSE 5000
CMD ["python3", "app.py"]

问题:

  • 使用 latest 标签

  • Ubuntu 镜像体积大

  • 多个 RUN 导致层多

  • 没有清理 apt 缓存

  • 使用 root 用户运行

  • 没有利用多阶段构建

优化后(生产级 Dockerfile)

# 多阶段构建 + 轻量镜像 + 安全实践
# 阶段1:构建依赖(可选,如果需要编译)
FROM python:3.9-slim AS builder
WORKDIR /install
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# 阶段2:运行
FROM python:3.9-slim
# 创建非 root 用户
RUN groupadd -r app && useradd -r -g app appuser

WORKDIR /app
# 从构建阶段复制已安装的包
COPY --from=builder --chown=appuser:app /install /usr/local
# 复制应用代码(设置所有权)
COPY --chown=appuser:app . .

USER appuser

EXPOSE 5000

CMD ["python", "app.py"]

或者更简单的单阶段优化(适用于无需编译的场景):

FROM python:3.9-slim

# 创建用户
RUN groupadd -r app && useradd -r -g app appuser

WORKDIR /app

# 先复制依赖文件(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再复制代码
COPY --chown=appuser:app . .

USER appuser

EXPOSE 5000

CMD ["python", "app.py"]

六、构建时优化技巧

1. 使用 BuildKit(Docker 18.09+)

启用 BuildKit 可以获得更快的构建速度和更多功能。

# 启用方式
export DOCKER_BUILDKIT=1
docker build -t myapp .

# 或在 /etc/docker/daemon.json 中配置
{
  "features": { "buildkit": true }
}

2. 并行构建阶段

BuildKit 可以并行执行互不依赖的构建阶段。

3. 使用外部缓存源(高级)

docker build --cache-from=myregistry/myapp:latest -t myapp .

七、检查镜像体积和层

# 查看镜像大小
docker images

# 查看镜像历史(每层大小)
docker history myimage

# 分析镜像内容(需要 dive 工具)
# 安装:brew install dive (macOS) / wget ... (Linux)
dive myimage

八、课后练习

练习 1:优化一个现有的 Dockerfile

给定以下 Dockerfile(有诸多问题),请优化它:

FROM node:latest
COPY . /app
WORKDIR /app
RUN npm install
RUN npm install -g nodemon
EXPOSE 3000
CMD ["nodemon", "app.js"]

优化要点:

  • 改用 node:18-alpine

  • 合并 RUN

  • 利用缓存(先复制 package.json)

  • 使用非 root 用户

  • 生产环境不需要 nodemon(改用 node)

练习 2:多阶段构建实践

编写一个 Go 应用的 Dockerfile(即使你不会 Go,可模拟):

  • 阶段1:从 golang:1.21-alpine 构建二进制文件

  • 阶段2:从 alpine:latest 运行该二进制文件

  • 最终镜像大小应小于 20MB(Go 二进制本身 + alpine 基础)

练习 3:安全检查

使用 docker scan 扫描你自己构建的镜像,找出高危漏洞并尝试修复(升级基础镜像版本)。


九、常见问题

Q1:Alpine 镜像中无法安装某些 Python 包?

A:Alpine 使用 musl libc,而许多二进制 wheel 是针对 glibc 构建的。解决方法:

  • 改用 python:3.9-slim(基于 Debian,稍大)

  • 或安装编译依赖:apk add gcc musl-dev python3-dev

Q2:多阶段构建中如何复制多个文件?

A:可以在前阶段用 WORKDIR 组织,然后用 COPY --from=builder /src /dest 复制整个目录。

Q3:ARG 真的安全吗?

A:不安全。ARG 值会保留在镜像历史中(docker history --no-trunc 可见)。永远不要用 ARG 传递密码、密钥。使用 Docker secrets 或外部机制。

Q4:如何减少构建时间?

A:

  • 利用构建缓存(把不常变的指令放前面)

  • 使用 BuildKit

  • 将依赖安装与代码复制分离

  • 使用 .dockerignore 减少上下文大小

更多推荐