4、构建与打包:使用Dockerfile构建镜像、多阶段构建优化、镜像仓库推送
好,咱们进入第四章节。前面我们写好了验证服务代码,也配好了环境依赖。但代码写出来是给人看的,跑起来才是给机器用的。怎么让它在任何机器上都能一键运行?这就是容器化要做的事。
说白了,Docker 就是把你的应用连同它的操作系统级依赖,打包成一个标准化的「集装箱」。我刚开始接触 Docker 时也觉得不就是个轻量级虚拟机嘛,后来踩了坑才明白——镜像体积、构建速度、安全漏洞,每一个都是大坑。
4.1 编写高效的 Dockerfile
Dockerfile 就是镜像的「配方」。但配方写得好不好,直接决定了镜像能不能用、好不好用。
4.1.1 基础镜像选择
我个人习惯,能用 Alpine 绝不用 Ubuntu。为什么?Alpine 只有 5MB 左右,而 Ubuntu 基础镜像动辄 200MB。对于验证服务这种无状态应用,我们只需要 Python 运行时和必要的系统库。
# 不推荐:体积大,攻击面多
FROM python:3.11-slim-bullseye
# 推荐:轻量,安全
FROM python:3.11-alpine3.18
4.1.2 指令顺序与层缓存
Docker 构建时,每条指令都会生成一个层。层有缓存机制——如果某条指令没变,Docker 会复用缓存层。所以,把不常变的部分放在前面,常变的部分放在后面。
# 1. 先安装系统依赖(几乎不变)
RUN apk add --no-cache gcc musl-dev
# 2. 再复制依赖声明文件(只有改依赖时才变)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. 最后复制源码(每次开发都会变)
COPY . /app
你想想看,如果先把源码复制进去,再装依赖,那你每次改一行代码,整个 pip install 都要重跑。我见过团队里有人构建一次镜像要 15 分钟,就是因为指令顺序写反了。
4.1.3 最佳实践清单
| 实践 | 说明 | 示例 |
|---|---|---|
| 使用 .dockerignore | 排除本地缓存、日志、git 目录 | __pycache__/ *.log .git/ |
| 合并 RUN 指令 | 减少层数,用 && 连接 |
RUN apk add X && pip install Y |
| 指定非 root 用户 | 降低容器逃逸风险 | USER appuser |
| 使用 LABEL | 记录镜像元数据 | LABEL maintainer="dev@example.com" |
4.2 多阶段构建:瘦身利器
这是我最想强调的部分。很多新手把编译工具链也带进最终镜像,结果一个验证服务镜像搞到 1.2GB。多阶段构建就是为了解决这个问题——编译环境一个阶段,运行环境另一个阶段,最终只保留运行产物。
4.2.1 为什么需要多阶段?
我们的验证服务可能需要编译一些 C 扩展(比如 numpy、pandas 的底层库)。编译时需要 gcc、python-dev,但运行时只需要编译好的 .so 文件。把这些编译工具留在镜像里,不仅浪费空间,还增加了安全漏洞风险。
4.2.2 实战:验证服务多阶段构建
# === 第一阶段:编译环境 ===
FROM python:3.11-alpine3.18 AS builder
WORKDIR /build
# 安装编译依赖
RUN apk add --no-cache gcc musl-dev linux-headers
# 复制依赖文件并安装到临时目录
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# === 第二阶段:运行环境 ===
FROM python:3.11-alpine3.18
WORKDIR /app
# 只从 builder 阶段复制安装好的包
COPY --from=builder /install /usr/local
# 复制验证服务源码
COPY src/ ./src/
COPY config/ ./config/
# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["python", "-m", "src.main"]
效果对比:单阶段构建镜像体积约 850MB,多阶段构建后仅 120MB。减少了 85% 的体积,部署速度提升 3 倍以上。
4.2.3 避坑指南
我曾经在复制阶段踩过一个坑:COPY --from=builder 的路径写错了,结果镜像里只有源码没有依赖,容器启动直接报 ModuleNotFoundError。排查了半天才发现是路径少了一层。
建议你在 builder 阶段先 RUN ls /install 确认产物路径,再写 COPY 指令。
4.3 镜像仓库推送
镜像构建好了,得放到一个地方让大家都能拉取。这就是镜像仓库的作用。
4.3.1 仓库类型选择
| 仓库 | 适用场景 | 特点 |
|---|---|---|
| Docker Hub | 个人/开源项目 | 免费但有拉取限制 |
| Harbor | 企业私有部署 | 支持 RBAC、漏洞扫描 |
| AWS ECR | AWS 云原生 | 与 IAM 集成 |
| GitLab Container Registry | CI/CD 集成 | 与 GitLab 项目绑定 |
我个人习惯,公司内部用 Harbor,个人项目用 Docker Hub。Harbor 的镜像复制功能特别好用——可以在不同机房之间同步镜像,容灾时直接切流量。
4.3.2 推送流程
- 登录仓库:
docker login registry.example.com - 打标签:
docker tag verify-service:latest registry.example.com/verify-service:v1.0.0 - 推送镜像:
docker push registry.example.com/verify-service:v1.0.0
标签规范建议:不要只用 latest。我见过生产环境因为 latest 被覆盖导致回滚失败的案例。建议使用语义化版本号 + Git commit SHA,比如 v1.0.0-abc1234,既能追溯版本,又能精确定位代码。
4.3.3 自动化推送脚本
#!/bin/bash
# push_image.sh
set -e
IMAGE_NAME="verify-service"
REGISTRY="registry.example.com"
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)
echo "构建镜像..."
docker build -t ${IMAGE_NAME}:${VERSION} .
echo "打标签..."
docker tag ${IMAGE_NAME}:${VERSION} ${REGISTRY}/${IMAGE_NAME}:${VERSION}
docker tag ${IMAGE_NAME}:${VERSION} ${REGISTRY}/${IMAGE_NAME}:${COMMIT}
echo "推送镜像..."
docker push ${REGISTRY}/${IMAGE_NAME}:${VERSION}
docker push ${REGISTRY}/${IMAGE_NAME}:${COMMIT}
echo "完成!"
4.4 本章知识体系
下面这张图把构建与打包的核心流程串起来了。你可以看到,从源码到可运行容器,中间经历了三个关键阶段:Dockerfile 编写、多阶段构建优化、镜像仓库推送。
嗯,到这里构建与打包的核心内容就讲完了。记住一个原则:镜像越小,启动越快,攻击面越窄。多阶段构建不是锦上添花,而是生产环境的必备手段。