← 返回模块
3.6.5.2beta 可读 · 未来付费校验通过内容版本 2026-05-24

量化服务的容器化

3.6.5 · 构建、部署与容器化 · 编程

上海一家 私募 的风控工程主管把你写的第一份 Containerfile 拿出来评审,还没读到第一行指令就问三个问题:「这镜像里有什么是 root 跑的?」「最终镜像 多大?」「构建是 在 镜像构建时 重新解析依赖,还是 直接 消费 L1 的 wheel?」每个问题都有一个 错答案 平台组 不会 部署——所有进程都跑 root、镜像 800 MB、RUN 里 直接 pip install confluent-kafka 从 PyPI 拉。每个问题也都有一个 对答案 本课 要 教 你 写 出来——runtime 阶段最后一条指令 USER appuser、最终镜像 ~150 MB、COPY --from=builder /build/dist/*.whl /tmp/ 把 L1 的 wheel 拷进来 不再重新解析。L1 的 wheel 是 输入;OCI 镜像 是 输出;Containerfile 是 本模块 后面 每一课 都引用的 七条 纪律 文档。

容器 到底 是 什么

容器 是 一个 Linux 进程,跑在 三个 Linux 原语 的 组合里——不是 VM。第一,​​命名空间​ 隔离 进程 看到的东西:PID(进程 看到 自己的 PID 1 init 树,不是 宿主的)、mount(自己的 文件系统 根)、network(自己的 接口 与 路由表)、UTS(自己的 主机名)、IPC(自己的 System V + POSIX IPC)、user(可选 的 UID/GID 映射 让 container 内的 root ≠ 宿主上的 root)。第二,​​cgroups​​(现代 内核 v2,老 RHEL v1)控制 资源:cpu.maxmemory.maxio.weight。第三,​​联合文件系统​​(现代 路径 用 overlayfs;废弃 路径 aufs / devicemapper)把 只读 的 基础 镜像 当 下层、可写 的 顶层 叠在 上面,多个 container 共享 不变的 基础 层。运维 上 一句话:container 是 一个 拥有 更严格 chroot、命名空间、资源 限制 的 进程——不是 VM;它 与 宿主 共享 内核,所以 内核 CVE 就是 container CVE。给 节点 打 补丁。

OCI:两份 规范,三个 CLI

每个 容器 工具 都 产出 与 消费 两份 互通 格式:OCI image specification(镜像 的 tarball-of-layers + manifest + config)和 OCI runtime specification(runtime 执行的 config.json + rootfs)。Docker、Podman、nerdctl 是 三个 几乎 等价 的 CLI,产出 与 消费 同样 的 OCI 镜像。docker build 构建 的 镜像 能 不改 跑 在 podman run 下;两边 任一 推到 registry 的 镜像,另一边 拉 都 不 需要 转换。

如何选。Docker 在 非 强监管 场景 仍是 modal,得益于 生态 惯性(docker composedocker buildx、Docker Desktop)。Podman 是 受 监管 环境 的 推荐 默认:默认 rootless、无 daemon、CLI 与 Docker 直接 兼容(alias docker=podman);不 暴露 dockerd 的 攻击面;在 RHEL / Anolis / openEuler / 麒麟 等 国产 基础设施 上 是 默认。nerdctl 是 containerd 自己的 CLI,常见 在 直接 跑 containerd 的 Kubernetes 节点 上。安全 等保 要求 严就 选 Podman;团队 已经 跑 Docker 就 留 Docker。OCI 镜像 是 同 一份。

生产 等级 的 Containerfile

以 L1 的 feed-handler wheel 为 例。两个 阶段,八条 纪律 都 在:

# syntax=docker/dockerfile:1.7

FROM python:3.12-slim-bookworm AS builder
RUN apt-get update && apt-get install --no-install-recommends -y \
        build-essential librdkafka-dev libpq-dev \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY pyproject.toml uv.lock .
COPY src/ ./src/
RUN pip install --no-cache-dir uv build && uv build --wheel

FROM python:3.12-slim-bookworm
RUN apt-get update && apt-get install --no-install-recommends -y \
        librdkafka1 libpq5 \
    && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
WORKDIR /app
COPY --from=builder --chown=appuser:appuser /build/dist/*.whl /tmp/
RUN pip install --user --no-cache-dir /tmp/*.whl && rm /tmp/*.whl
ENV PATH="/home/appuser/.local/bin:$PATH"
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import feed_handler" || exit 1
ENTRYPOINT ["python", "-m", "feed_handler"]
CMD ["consumer"]

八条 纪律 逐条 解读

按 评审 清单 的 顺序,逐行 看:

  • multi-stage build——两个 FROM 阶段,build 工具 绝不 进 最终 镜像。builder 阶段 装 build-essential librdkafka-dev libpq-dev(编译器 + 开发 头文件);runtime 阶段 只 装 librdkafka1 libpq5(wheel 在 运行时 链接 的 共享 库)。这 一步 把 最终 镜像 从 ~700 MB 砍 到 ~150 MB。
  • pinned base image——python:3.12-slim-bookworm,绝不 python:latest。浮动 tag 破坏 可重现:今天 构建 的 镜像 三个月 后 CI 重建 出来 就 不一样。
  • single RUN for apt install + cleanup——一个 RUNapt-get update && apt-get install --no-install-recommends -y ... && rm -rf /var/lib/apt/lists/*--no-install-recommends 砍掉 ~50 MB 的 recommends;rm -rf 不让 apt 索引(~30 MB)留 在 这一层。
  • COPY pyproject.toml uv.lock . before COPY src/——依赖 安装 层 在 源码 编辑 时 能 命中 缓存。CI 里 最常 见 的 构建 是 改 一行 producer.py,这时 依赖 层 应 命中 缓存;warm cache ~10 秒,cold cache ~90 秒。
  • USER appuser——useradd --create-home --shell /bin/bash appuserUSER appuser,让 container 内 所有 进程 跑 在 UID 1000。最终 镜像 绝不 USER rootdocker run --rm <image> id 若 返回 uid=0(root),说明 这条 漏 了。
  • COPY --chown——COPY --from=builder --chown=appuser:appuser /build/dist/*.whl /tmp/ 省 掉 一个 单独 RUN chown 层。少 一层,少 一次 文件系统 遍历。
  • HEALTHCHECK——HEALTHCHECK --interval=30s --timeout=5s CMD python -c "import feed_handler" || exit 1,让 编排器 能 探测 坏 镜像。Kubernetes 通过 readinessProbe 镜像 这个 检查;compose 通过 depends_on: condition: service_healthy 拿来 用。检查 写 得 极 简,更深 的 就绪 逻辑 属于 readinessProbe,不 属于 Containerfile。
  • ENTRYPOINT [...] + CMD [...] exec-form separation——ENTRYPOINT ["python", "-m", "feed_handler"]CMD ["consumer"]。运维 在 运行时 覆盖 CMDdocker run feed-handler:1.0.0 producer 拼 出 python -m feed_handler producer。exec-form 不 绕 shell,docker stop 发的 SIGTERM 直达 Python 进程。

.dockerignore

Docker / Podman 把 构建 上下文 送 给 daemon 之前,.dockerignore 把 不该 上传 的 都 滤掉。一个 典型 的 Python 项目 把 上传 从 ~200 MB 砍 到 ~5 MB:

.git
.gitignore
dist/
build/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
venv/
*.egg-info
*.md
tests/
.github/
manifests/
.env
.env.*

构建、检查、扫描

五条 命令 的 验证 序列,按 这个 顺序:

docker buildx build --tag feed-handler:1.0.0 --tag feed-handler:1.0.0-sha-$(git rev-parse --short HEAD) --load .
docker history feed-handler:1.0.0
docker run --rm feed-handler:1.0.0 id
docker run --rm feed-handler:1.0.0 producer
trivy image --severity HIGH,CRITICAL --exit-code 1 feed-handler:1.0.0

docker buildx build 同时 打 两 个 tag——feed-handler:1.0.0(取 自 L1 的 __version__ semver)加 feed-handler:1.0.0-sha-$(git rev-parse --short HEAD)(不可变 的 git-SHA tag 用 来 溯源)。docker history 按 层 读 大小——确认 没有 层 超过 200 MB。docker run --rm feed-handler:1.0.0 id 必须 返回 uid=1000(appuser) gid=1000(appuser) groups=1000(appuser);若 返回 uid=0(root)USER appuser 这一行 漏 了。docker run --rm feed-handler:1.0.0 producer 覆盖 CMD 验证 entrypoint 已经 解析。trivy image --severity HIGH,CRITICAL --exit-code 1 是 标准 CI 闸门;任何 HIGH / CRITICAL 且 有 修复 的 CVE 都 让 构建 失败。

运维 量化 锚点

四 个 你 应 记 住 的 数 字,便于 凭 大小 诊断 坏 Containerfile:

  • final image size ~150 MB(slim Python 多 阶段 构建)。
  • build time cold cache ~90 s(首次 构建,下载 + 安装 依赖)。
  • build time warm cache ~10 s(只 改 源码,依赖 层 命中 缓存)。
  • docker history shows no layer > 200 MB(依赖 层 最大;若 有 哪层 更大,就 有 问题)。

诊断 规则:若 镜像 800 MB,多 阶段 边界 破 了(build 工具 进 了 最终 阶段);若 warm cache 构建 5 分钟,COPY 顺序 错 了(改 源码 时 让 依赖 层 失效);若 id 返回 uid=0USER appuser 漏 了。改 Containerfile,不要 用 docker build --no-cache 掩盖 问题。

凭据 纪律

两条 支柱,按 这个 顺序:

  • build-time secrets via BuildKit RUN --mount=type=secret,id=<name>——pip install 时 需要 私有 索引 凭据,就 把 secret 以 文件 形式 挂到 build 内的 /run/secrets/<name>。secret 绝不 进 任何 一 层。调用 方 写 docker buildx build --secret id=pip-auth,src=$HOME/.pip-auth .
  • runtime secrets via the orchestrator: compose env_file or Kubernetes Secret——运行时 凭据(Postgres 密码、Kafka SASL 凭据)来自 编排器。compose 通过 env_file: 从 gitignored .env 拉;Kubernetes 把 Secret 挂 成 环境变量 或 /etc/secrets/<name> 下的 文件。指向 L3。

合 规则:Containerfile ​绝不​ 含 凭据;secret 在 镜像 之外,构建时 走 BuildKit,运行时 走 编排器。

标签 纪律

生产 镜像 标签 是 registry.example.com/quant/feed-handler:1.2.3,从 L1 的 semver 来。CI 同时 打 :1.2.3-sha-abc1234abc1234 = git short SHA)做 溯源,让 已 部署 的 镜像 能 还原 到 源码 commit。生产 绝不 :latest:滚动 更新 与 回滚 都 需要 编排器 能 锁住 的 不可变 tag。多 架构 构建(docker buildx build --platform linux/amd64,linux/arm64 --push .)作为 混合 架构 集群 的 生产 CI 命令,点 到 为止。

镜像 扫描

trivy image --severity HIGH,CRITICAL --exit-code 1 <image> 是 标准 CI 闸门;备选 扫描器 grypesnyk container test、GitHub 自带 codeql 配套 镜像 扫描 点 到 为止。SBOM 生成(syft package <image>)与 供应链 证明(cosign sign / sigstore / SLSA 等级)是 SRE / 安全 工程 的 进阶 命题,点 到 为止。

纪律 总结

多 阶段 构建。非 root 用户。锁 死 基础 镜像。按 COPY 顺序 缓存 层。不 把 凭据 烤 进 镜像。按 semver + commit SHA 打 标签。.dockerignore 让 上下文 瘦身。CI 内 扫描。Containerfile 按 3.6.2 当 受 评审 代码。

练习

Exercise

取 L1 的 feed-handler 包(python -m build 产出的 wheel 与 uv.lock),(a) 按 本课 完整 形态 写 Containerfile——两个 FROM 阶段、两次 apt 安装、useraddUSER appuserCOPY --from=builder 上的 --chownHEALTHCHECKENTRYPOINTCMD 分离。(b) 按 本课 写 .dockerignore。(c) 用 docker buildx build --tag feed-handler:1.0.0 --load . 构建 并 计 时(写下 墙钟 秒数)。(d) 用 docker run --rm feed-handler:1.0.0 id 验证 安全 基线,确认 输出 是 uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)(不是 uid=0(root))。(e) 用 docker history feed-handler:1.0.0 看 层,截图 或 贴出 输出;写下 最终 镜像 大小。镜像 应 约 150 MB;若 800 MB,调试 你的 多 阶段 边界。(f) 在 运行时 覆盖 CMD 验证 子命令 模式:docker run --rm feed-handler:1.0.0 producer 跑 producer 子命令(还 没 Kafka broker 也行,重点 是 entrypoint 解析 正确)。(g) 用 trivy image --severity HIGH,CRITICAL --exit-code 1 feed-handler:1.0.0 扫描 镜像,确认 退出 码 0(没有 应处理 的 HIGH/CRITICAL 漏洞);若 有 报 出,写下 哪些 包 版本 需要 提升。(h) 按 双 tag semver + SHA 模式 打 tag:docker tag feed-handler:1.0.0 feed-handler:1.0.0-sha-$(git rev-parse --short HEAD),通过 docker image ls feed-handler 验证 两 个 tag 都 在。(i) 只 改 src/feed_handler/producer.py(改 一行),重新 构建 并 计 时;确认 在 30 秒 内(warm cache 应 命中 依赖 层);若 不 是,调试 你的 COPY 顺序。

提示
若 最终 镜像 远 大于 150 MB,跑 docker history feed-handler:1.0.0 看 哪 层 带 着 编译器 或 dev 头文件——这 说明 builder 阶段 漏 进 了 runtime 阶段。runtime 阶段 只 应 装 librdkafka1 libpq5,绝不 build-essentiallibrdkafka-dev
提示
若 warm-cache 重建 慢,COPY 顺序 错 了。确认 COPY pyproject.toml uv.lock .COPY src/ ./src/ 之前,让 依赖 安装 层 能 在 源码 编辑 时 命中 缓存。

必备 组件 回顾

本课 交付物 对 合约 的 映射:

  1. Fenced ```dockerfile 块——完整 的 多 阶段 Containerfile,两 个 FROM python:3.12-slim-bookworm 阶段、两 次 apt 安装(builder 阶段 build-essential librdkafka-dev libpq-dev、runtime 阶段 librdkafka1 libpq5)、useradd --create-home --shell /bin/bash appuserUSER appuserCOPY --from=builder --chown=appuser:appuser /build/dist/*.whl /tmp/HEALTHCHECK ... CMD python -c "import feed_handler" || exit 1ENTRYPOINT ["python", "-m", "feed_handler"]CMD ["consumer"]
  2. Fenced ``` 块——.dockerignore.git.gitignoredist/build/__pycache__/*.pyc.pytest_cache/.mypy_cache/.ruff_cache/.venv/venv/*.egg-info*.mdtests/.github/manifests/.env.env.*
  3. Fenced ```bash 块——五 条 构建 + 检查 + 扫描 命令,顺序:docker buildx build --tag feed-handler:1.0.0 --tag feed-handler:1.0.0-sha-$(git rev-parse --short HEAD) --load .docker history feed-handler:1.0.0docker run --rm feed-handler:1.0.0 iddocker run --rm feed-handler:1.0.0 producertrivy image --severity HIGH,CRITICAL --exit-code 1 feed-handler:1.0.0
  4. Inline-code 列表 八 条 Containerfile 纪律:multi-stage buildpinned base imagesingle RUN for apt install + cleanupCOPY pyproject.toml uv.lock . before COPY src/USER appuserCOPY --chownHEALTHCHECKENTRYPOINT [...] + CMD [...] exec-form separation
  5. Inline-code 列表 四 个 大小 + 速度 运维 锚点:final image size ~150 MBbuild time cold cache ~90 sbuild time warm cache ~10 sdocker history shows no layer > 200 MB
  6. Inline-code 列表 两 条 凭据 纪律 支柱:build-time secrets via BuildKit RUN --mount=type=secret,id=<name>runtime secrets via the orchestrator: compose env_file or Kubernetes Secret
  7. 上面 的 练习 加 两 个 渐进式 Hint。

中国 区 锚点

国内 量化 firm 的 Containerfile 走 同 一 套 沪深300 ETF(510300)合成 流 形态:worked example 容器 跑 的 是 平台 组 在 自建 集群 或 阿里云 ACK 上 部署 给 沪市 SSE / 深市 SZSE 行情 入口 的 镜像。CVE 闸 --severity HIGH,CRITICAL --exit-code 1 与 等保 / 数据 安全 法 / 个人 信息 保护 法 下 私募 与 公募 的 合规 实践 一致;cosign sign 这 条 forward-pointer 对应 国内 头部 量化 firm 已经 在 跑 的 镜像 签名 流程。同 一 个 Trivy 闸 跑 在 CFFEX 期指 IF / IC / IH、上证 50ETF / 沪深300ETF、深证 创业板 ETF 任 一 流 的 feed handler 上 都 同 形——Containerfile 是 代码 的 属性 不 是 标的 的 属性,平台 组 评审 的 是 同 一 套 清单,T+1 结算 与 涨跌停 的 微 结构 也 不 影响 镜像 形态。

阅读 清单

Docker 中文 文档 社区 翻译 yeasy.gitbook.io/docker_practice/;Podman 中文 文档 社区 译本;Kubernetes 官方 中文 文档 容器 章节 kubernetes.io/zh-cn/docs/concepts/containers/;极客 时间《容器实战高手课》系列;极客 时间《Docker 实战 与 镜像 优化》系列;阿里云 ACR 文档 help.aliyun.com/product/60716.html;腾讯云 TCR 文档;公有 云 厂商 容器 镜像 服务 帮助 中心;Harbor 中文 用户 手册 goharbor.io/docs/;Trivy 中文 摘要 社区 翻译。一条 额外 注释:国内 量化 firm 的 Containerfile 通常 由 quant developer 自己 写,但 基础 镜像 + apt mirror 配置 + Harbor endpoint 来自 平台 / DevOps 团队 的 模板;quant 在 模板 上 加 业务 层。私募 与 公募 在 这 一 步 上 的 工程 实践 高度 一致——基础 镜像 内 嵌 公司 CA、apt 走 内网 mirror、Trivy 报告 进 内部 漏洞 看板。

通往 L3 的 桥

下一课 拿 本 Containerfile 产出 的 镜像,写 compose 文件 与 Kubernetes manifest 让 它 跑 起来,并 加 上 每个 生产 部署 都 必 须 应用 的 资源 请求 与 就绪 探针 纪律。