实际的场景:harbor 主备双战,每次拉取的时候拉到备战导致镜像拉取失败,最终排查是因为镜像包太大了,主站在往备战同步的时候失败或者太慢,导致K8s 拉不到镜像。 运维建议缩减镜像的大小。
镜像内容:python 以及 项目依赖的.venv 虚拟环境
原Dockerfile
bash
FROM harbor.xxx.com/ai-engineering/python:3.12.10-slim
# 时区配置
ENV TZ=Asia/Shanghai
# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/backend/.venv/bin:$PATH" \
UV_INDEX_URL="https://assets.hwwt2.com/repository/pypi-public/simple" \
UV_EXTRA_INDEX_URL="https://mirrors.aliyun.com/pypi/simple/"
# 安装基本系统依赖
# curl: 用于网络请求测试等
# git: 如果依赖项中有 git 仓库引用则需要
RUN if command -v apt-get >/dev/null 2>&1; then \
if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources; \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources; \
elif [ -f /etc/apt/sources.list ]; then \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list; \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list; \
fi; \
apt-get update && apt-get install -y --no-install-recommends curl git && rm -rf /var/lib/apt/lists/*; \
elif command -v microdnf >/dev/null 2>&1; then \
microdnf install -y curl git ca-certificates gcc gcc-c++ && microdnf clean all; \
elif command -v yum >/dev/null 2>&1; then \
yum install -y curl git ca-certificates gcc gcc-c++ && yum clean all; \
elif command -v apk >/dev/null 2>&1; then \
apk add --no-cache curl git ca-certificates gcc g++; \
else \
echo "Error: No supported package manager found (apt-get, microdnf, yum, apk)" && exit 1; \
fi
# 安装 uv
RUN python3 -m pip install --upgrade pip && python3 -m pip install uv
# 设置工作目录
WORKDIR /app
# 复制项目依赖配置文件
# 我们先只复制依赖文件以利用 Docker 缓存层
COPY backend/pyproject.toml backend/uv.lock* ./backend/
# 安装依赖
WORKDIR /app/backend
# 打印版本信息和网络连通性检查
RUN uv --version && python3 --version && \
echo "Checking network connectivity..." && \
curl -I -m 5 https://assets.hwwt2.com/repository/pypi-public/simple || echo "WARNING: Failed to connect to primary PyPI source"
# 调试:检查文件是否存在
RUN ls -la
# 创建虚拟环境
RUN python3 -m venv .venv
# Step 1: Install dependencies using uv export to respect uv.lock
# 使用 uv export 生成 requirements.txt 并安装,以确保使用锁定版本的依赖
RUN uv export --frozen --no-emit-project --format requirements-txt > requirements.txt && \
uv pip install --no-cache -p .venv/bin/python -r requirements.txt && \
rm requirements.txt
# 复制项目其余文件
WORKDIR /app
COPY . .
# 安装项目本身
WORKDIR /app/backend
RUN uv pip install --no-cache -p .venv/bin/python .
# 创建必要的临时目录并建立软链接
# 这是一个特定于业务逻辑的设置
RUN mkdir -p /app/backend/tmp/workshop/output /tmp/workshop && \
ln -sf /app/backend/tmp/workshop/output /tmp/workshop/output
# 暴露应用端口
EXPOSE 8080
# 切换到 backend 目录并启动应用
WORKDIR /app/backend
CMD ["/bin/bash", "-c", "python3 -m src.app"]
优化的Dockerfile
bash
# Stage 1: Builder
# 用于构建依赖和编译环境
FROM harbor.xxx.com/ai-engineering/python:3.12.10-slim AS builder
# 时区配置和构建相关的环境变量
ENV TZ=Asia/Shanghai \
UV_INDEX_URL="https://assets.hwwt2.com/repository/pypi-public/simple" \
UV_EXTRA_INDEX_URL="https://mirrors.aliyun.com/pypi/simple/"
# 安装构建依赖 (git, gcc, g++ 等)
# 相比原版,这里显式安装 apt 依赖,因为基础镜像是 debian based slim
# 同时替换为阿里云源以加速下载
RUN if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources; \
else \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list; \
fi && \
apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
gcc \
g++ \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# 安装 uv
RUN python3 -m pip install --upgrade pip && python3 -m pip install uv
# 设置工作目录
WORKDIR /app
# 复制项目依赖配置文件
COPY backend/pyproject.toml backend/uv.lock* ./backend/
# 创建虚拟环境并安装依赖
WORKDIR /app/backend
RUN python3 -m venv .venv
# 使用 uv export 生成 requirements.txt 并安装
# 这里在 builder 阶段安装所有依赖(包括编译型依赖)
RUN uv export --frozen --no-emit-project --format requirements-txt > requirements.txt && \
uv pip install --no-cache -p .venv/bin/python -r requirements.txt
# 复制项目其余文件 (用于安装项目本身)
WORKDIR /app
COPY . .
# 安装项目本身到虚拟环境
WORKDIR /app/backend
RUN uv pip install --no-cache -p .venv/bin/python --no-deps .
# Stage 2: Runtime
# 用于最终运行的精简镜像
FROM harbor.xxxx.com/ai-engineering/python:3.12.10-slim AS runtime
# 运行时环境变量
ENV TZ=Asia/Shanghai \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/app/backend/.venv/bin:$PATH"
# 安装运行时系统依赖 (仅保留 curl 用于健康检查等,去除 gcc/git)
RUN if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources; \
else \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list; \
fi && \
apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 复制项目代码
# 注意:先复制所有代码,再覆盖虚拟环境
COPY . .
# 从 builder 阶段复制构建好的虚拟环境
COPY --from=builder /app/backend/.venv /app/backend/.venv
# 创建必要的临时目录并建立软链接
WORKDIR /app/backend
RUN mkdir -p /app/backend/tmp/workshop/output /tmp/workshop && \
ln -sf /app/backend/tmp/workshop/output /tmp/workshop/output
# 暴露应用端口
EXPOSE 8080
# 启动应用
CMD ["/bin/bash", "-c", "python3 -m src.app"]
多阶段构建如何解决?
阶段 1:Builder 阶段(负责"重活")
基于 slim 或 full 镜像;
安装 gcc、python3-dev、git 等构建依赖;
下载并编译所有 Python 包(包括 C/C++ 扩展);
把编译好的 .so 文件、Python 模块等安装到一个干净目录(如 /install)。
阶段 2:Runtime 阶段(只放"必需品")
重新 FROM python:3.12.10-slim(干净起点);
只安装运行时最小依赖(如 curl, ca-certificates);
从 builder 阶段 COPY 已编译好的依赖(/install → site-packages);
COPY 应用代码;
不包含任何编译工具、缓存、虚拟环境结构。
通俗易懂的原理
❌ 传统方式(单阶段构建):
你请了一个厨师(Docker 镜像),他带着:
所有厨具(锅、刀、炉子 → 相当于 gcc, g++)
所有食材(米、菜、肉 → 相当于 torch, transformers)
还有一大堆包装盒、塑料袋、说明书(.venv 结构、缓存、临时文件)
做完饭后,你把整个厨房(包括不用的工具和垃圾)一起打包带走。
→ 虽然你只需要"那碗饭",但背了个 1.3GB 的大背包!
✅ 多阶段构建(聪明的做法):
你分两步:
第一步:在厨房里做饭(Builder 阶段)
厨师用全套工具把饭做好,只把做好的饭装进一个干净饭盒。
第二步:只带走饭盒(Runtime 阶段)
你扔掉所有锅碗瓢盆、包装垃圾,只拿走那个饭盒去吃。
→ 现在你背的只有 500MB 的饭盒,轻便又干净!
💡 关键区别:
饭 = 你的程序 + 必要依赖(如 torch) → 这部分没法省。
锅、垃圾、包装盒 = 编译工具、虚拟环境、缓存 → 这些运行时根本用不到,却占了大半体积!
多阶段构建就是:做完饭,只留饭,扔掉厨房。
那怎么做到两个FROM 切换的
Docker 构建时,会按顺序执行每个 FROM 开始的"独立小世界",最后只保留最后一个 FROM 的结果作为最终镜像。
但你可以从前面的小世界里"偷东西"过来(用 COPY --from=xxx)。
举个生活例子:做蛋糕
你想做一个干净漂亮的蛋糕(最终镜像),但做蛋糕需要厨房(Builder)。
步骤 1:在厨房里做蛋糕(第一个 FROM ... AS builder)
你在一个大厨房里(Linux 环境 #1);
用面粉、鸡蛋、烤箱(gcc、pip、依赖)做出一个蛋糕;
蛋糕做好了,放在盘子里。
步骤 2:换到客厅摆盘(第二个 FROM ... AS runtime)
你现在进入一个全新的、干净的客厅(Linux 环境 #2);
客厅里什么都没有(只有基础 Python);
但你可以从厨房里把蛋糕端过来
bash
COPY --from=builder /cake ./cake # 把厨房的蛋糕拿过来
最后,你只把客厅的样子打包送人 ------ 厨房和厨具全部不要了!
✅ 所以:两个 FROM = 两个完全独立的房间(容器构建环境)
Docker 先建好"厨房",再建"客厅",最后只交付"客厅"。
🔧 技术上 Docker 是怎么做到的?
Docker 按顺序处理每个 FROM 块,把它当作一个临时镜像来构建;
每个块有自己的文件系统、环境变量、已安装的包;
当遇到 COPY --from=builder ... 时,Docker 会:
暂停当前阶段(runtime);
回头去已经构建好的 builder 镜像里;
把指定的文件复制出来,放进当前阶段;
所有阶段构建完后,只有最后一个 FROM 的内容成为最终镜像,前面的阶段只留文件,不留环境。
⚠️ 注意:两个 FROM 必须用相同或兼容的基础镜像!
实际效果并没有达到理想

基础镜像其实并不大

看了venv Lib 依赖,包特别大,说明项目依赖的包很大
: .venv 中的 Lib (准确说是 site-packages )是混合体,但核心涉及系统兼容性的部分是"动态链接库"。

运维提供的解决方案:主备同推,然后等待运维解决大同步问题 😬
后期优化的方向
