3、vLLM基础镜像制作:Dockerfile编写、多阶段构建、镜像瘦身技巧

好,咱们进入第三章。镜像制作这事儿,说白了就是给vLLM搭个“窝”。窝搭得太大,传输慢、启动慢;窝搭得太糙,跑起来各种报错。我刚开始搞K8s部署时,就吃过镜像太大的亏——一个镜像几个G,拉取半小时,Pod调度都卡成PPT。后来才明白,镜像瘦身不是炫技,是生产环境的刚需。

3.1 为什么需要自制基础镜像?

官方镜像能用吗?能用。但生产环境里,你总会遇到这些情况:

  • 需要集成自定义的tokenizer或预处理逻辑
  • 要打安全补丁、装监控agent
  • 想控制CUDA版本、Python版本、系统库版本
  • 镜像太大,需要针对特定硬件做裁剪

嗯,说白了,官方镜像是“通用款”,咱们要的是“定制款”。

3.2 Dockerfile编写:从零搭一个vLLM镜像

先看一个我常用的Dockerfile模板。这个版本我迭代过十几次,踩了不少坑。

# ========== 第一阶段:构建阶段 ==========
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS builder

# 我个人习惯用apt装基础工具,不用miniconda
# 因为conda的依赖解析太慢,而且镜像层数多
RUN apt-get update && apt-get install -y \
    python3.10 python3.10-dev python3-pip \
    git build-essential \
    && rm -rf /var/lib/apt/lists/*

# 设置Python软链,避免版本混乱
RUN ln -sf /usr/bin/python3.10 /usr/bin/python

# 先装PyTorch,再装vLLM
# 注意:CUDA版本要和宿主机驱动匹配
RUN pip install --no-cache-dir torch==2.1.0 --index-url https://download.pytorch.org/whl/cu121

# 安装vLLM及其依赖
# 这里用--no-build-isolation可以加速,但要注意依赖冲突
COPY requirements.txt .
RUN pip install --no-cache-dir --no-build-isolation -r requirements.txt

# ========== 第二阶段:运行阶段 ==========
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04

# 只复制必要的Python环境和vLLM包
COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# 创建非root用户,安全第一
RUN useradd -m -u 1000 vllm && \
    mkdir -p /models /cache && \
    chown -R vllm:vllm /models /cache

USER vllm
WORKDIR /app

# 暴露vLLM默认端口
EXPOSE 8000

# 入口脚本,支持动态配置
COPY entrypoint.sh .
ENTRYPOINT ["bash", "entrypoint.sh"]
我的小技巧:requirements.txt里不要写死版本号,用“>=”加下限。比如vllm>=0.4.0。这样后续升级小版本时不用改Dockerfile,重新构建就行。

3.3 多阶段构建:为什么是两阶段?

你想想看,一个完整的vLLM编译环境有多大?CUDA工具链、gcc、cmake、各种头文件……加起来轻松超过5GB。但运行时只需要编译好的.so文件和Python包。

多阶段构建的核心思想就是:编译和运行分离

  • 第一阶段(builder):装所有编译工具、下载源码、编译。这个阶段可以很“胖”。
  • 第二阶段(runtime):只复制编译产物和运行时依赖。这个阶段要“瘦”。

我曾经犯过一个错:把builder阶段的apt缓存也复制到了runtime阶段。结果镜像里多了几百MB的deb包缓存。后来加了rm -rf /var/lib/apt/lists/*才解决。

3.4 镜像瘦身技巧:从5GB到1.2GB

嗯,这里才是重点。我总结了几条实战经验:

技巧 说明 效果
多阶段构建 编译和运行分离 减少2-3GB
清理apt缓存 每层apt install后都清理 减少200-500MB
使用--no-cache-dir pip不缓存wheel包 减少300-800MB
合并RUN指令 减少镜像层数 减少100-200MB
删除不必要的CUDA组件 只保留runtime,不要devel 减少1-2GB
使用slim基础镜像 比如python:3.10-slim 减少300-500MB
核心原则:镜像里只放“运行时必须”的东西。任何编译工具、文档、缓存、测试数据,统统不要。

3.5 避坑指南:我踩过的三个坑

讲几个真实案例,你遇到了能少走弯路:

  1. CUDA版本不匹配:我曾经用CUDA 11.8的镜像,但宿主机驱动只支持12.0。结果vLLM启动时直接报“CUDA driver version is insufficient”。后来我统一用nvidia-smi先查驱动版本,再选对应的CUDA镜像。
  2. Python版本冲突:系统自带的Python 3.8和vLLM要求的3.10打架。我一开始用update-alternatives切换,但总出问题。后来干脆在Dockerfile里硬编码python3.10,省心。
  3. 镜像层数过多:每个RUN指令都会生成一层。我有个同事一个Dockerfile写了20个RUN,镜像构建慢得像蜗牛。合并成3-4个RUN后,构建时间从15分钟降到3分钟。
特别注意:不要用apt-get upgrade升级所有包!这会把基础镜像的依赖关系搞乱,而且每次构建都会拉新版本,导致镜像不可复现。只升级你需要的安全补丁即可。

3.6 验证镜像:构建完怎么测?

镜像构建完,别急着推仓库。先本地跑一下:

# 构建镜像
docker build -t vllm-custom:1.0 .

# 查看镜像大小
docker images vllm-custom:1.0

# 启动容器,测试vLLM能否正常加载
docker run --gpus all -p 8000:8000 \
    -v /path/to/models:/models \
    vllm-custom:1.0 \
    --model /models/llama-7b \
    --port 8000

# 用curl测试API
curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{"model": "default", "prompt": "Hello", "max_tokens": 50}'

如果这一步能跑通,镜像基本就稳了。我习惯在CI/CD流程里加这一步,避免推到仓库才发现问题。

3.7 本章知识体系

下面这张图,是我梳理的镜像制作核心逻辑。你看一眼,心里就有谱了:

vLLM基础镜像制作核心流程 第一阶段:构建 (builder) • 基础镜像:nvidia/cuda:12.1.0-runtime • 安装:Python 3.10、pip、git、build-essential • 安装:PyTorch 2.1.0 (CUDA 12.1) • 安装:vLLM + 依赖 (--no-cache-dir) • 产物:/usr/local/lib/python3.10/dist-packages 镜像瘦身技巧 • 多阶段构建:编译运行分离 • 清理apt缓存:rm -rf /var/lib/apt/lists/* • pip --no-cache-dir:不缓存wheel • 合并RUN指令:减少镜像层数 • 使用runtime镜像,不用devel 第二阶段:运行 (runtime) • 基础镜像:nvidia/cuda:12.1.0-runtime (更小) • 复制:dist-packages + /usr/local/bin • 创建非root用户:vllm (UID 1000) • 暴露端口8000,设置entrypoint 最终镜像:约1.2GB(原5GB)

这张图把整个流程串起来了。你照着这个思路写Dockerfile,基本不会跑偏。

最后说一句:镜像瘦身不是一锤子买卖。每次升级vLLM版本、换CUDA版本,都要重新审视镜像大小。我习惯在CI里加一个“镜像大小检查”,超过2GB就告警。这样能及时发现“镜像膨胀”的问题。

好,这一章就到这里。镜像做好了,下一章咱们聊聊怎么把它部署到K8s上,以及怎么管理多个模型版本。


公众号:蓝海资料掘金营,微信deep3321