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
注意: Alpine 使用 musl libc 而非 glibc。如果你的验证服务依赖了某些 C 扩展(比如用到了特定硬件驱动库),可能会遇到兼容性问题。我在一个项目中就遇到过这种情况,最后不得不切回 slim 版本。

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 推送流程

  1. 登录仓库:docker login registry.example.com
  2. 打标签:docker tag verify-service:latest registry.example.com/verify-service:v1.0.0
  3. 推送镜像: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 编写、多阶段构建优化、镜像仓库推送。

构建与打包核心流程 构建与打包核心流程 源码 + 依赖 Dockerfile 编写 多阶段构建 优化后镜像 镜像仓库 生产部署 关键优化点 • 基础镜像选择 Alpine • 指令顺序利用缓存 • 多阶段分离编译/运行

嗯,到这里构建与打包的核心内容就讲完了。记住一个原则:镜像越小,启动越快,攻击面越窄。多阶段构建不是锦上添花,而是生产环境的必备手段。