容器化部署Docker打包LLM应用的最佳实践从一次凌晨三点的事故说起凌晨三点手机震得我直接从床上弹起来。线上LLM推理服务挂了日志里只有一行“OOM Killer terminated process”。我盯着屏幕骂了句脏话——明明本地测试好好的怎么一上生产就崩后来排查发现问题出在Docker镜像里。我图省事把整个conda环境连同几十个G的模型权重一股脑塞进镜像结果容器启动时内存直接爆了。更蠢的是我连模型文件都没做分层处理每次重新部署都要重新下载整个镜像团队同事看我的眼神都不对了。从那以后我花了整整两周重构了LLM应用的Docker化方案。今天这篇笔记就是那次事故后沉淀下来的血泪经验。镜像瘦身别把整个家当都装进去很多新手打包LLM应用时习惯性用pip freeze requirements.txt然后COPY整个项目目录。这做法在普通Web应用上勉强能跑但LLM应用动辄几个G的模型文件这么搞就是找死。正确的做法是分层构建。模型权重这种几乎不变的大文件单独放在一个基础镜像层里。代码和依赖这种频繁更新的小文件放在上层。这样每次更新代码时Docker只需要重新构建上层模型层直接复用缓存。看个实际例子我现在的Dockerfile长这样# 基础镜像选轻量的别用python:3.10-slim这种带一堆工具链的 FROM python:3.10-slim AS base # 先装系统依赖这里踩过坑——apt-get update和install要写在同一行 # 否则Docker会缓存update层导致后续安装失败 RUN apt-get update apt-get install -y \ libgomp1 \ rm -rf /var/lib/apt/lists/* # 单独一层放模型文件注意这里用COPY而不是ADD # ADD会自动解压tar.gz但模型文件不需要解压反而可能出问题 FROM base AS model-layer COPY ./models /app/models # 代码和依赖放上层这样改代码不用重新下载模型 FROM model-layer AS app-layer WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY ./src /app/src # 最后设置非root用户运行别问为什么安全 RUN useradd -m -u 1000 appuser chown -R appuser:appuser /app USER appuser CMD [python, src/serve.py]这个Dockerfile构建出来的镜像从原来的12G瘦身到3.5G。更关键的是改代码时构建时间从15分钟缩短到30秒。内存管理别让模型把容器撑爆LLM推理最头疼的就是内存。模型加载时显存和内存的消耗是动态的Docker默认的--memory限制如果设得太死模型加载到一半直接OOM设得太松又可能把宿主机资源吃光。我的经验是分两步走。第一步在Dockerfile里设置环境变量让模型框架自己控制内存# 这里别这样写ENV OMP_NUM_THREADS4 # 应该根据容器CPU核数动态设置否则换机器就崩 ENV OMP_NUM_THREADS${NPROC:-4} ENV MALLOC_TRIM_THRESHOLD_131072 ENV PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128第二步在docker-compose或K8s配置里给容器设置合理的资源限制。我一般这样配services:llm-service:image:llm-app:latestdeploy:resources:limits:memory:8Gcpus:4reservations:memory:4Gcpus:2environment:-CUDA_VISIBLE_DEVICES0-MODEL_CACHE_DIR/app/modelsvolumes:-model-cache:/app/models注意那个reservations字段它告诉调度器这个容器至少需要4G内存但允许它用到8G。这样既保证了模型加载时有足够的缓冲又不会无限制地吃资源。模型加载优化别每次都重新加载LLM应用启动慢很大一部分原因是模型加载。一个7B的模型从磁盘加载到显存少说也要30秒。如果每次容器重启都来一遍用户体验直接爆炸。解决方案是使用卷挂载。把模型文件放在宿主机的一个目录里通过Docker volume挂载到容器内。这样容器重启时模型文件还在不需要重新下载。但这里有个坑——模型文件权限。我之前遇到过容器内用户是appuserUID 1000但宿主机上的模型文件是root所有导致容器启动时权限不足模型加载失败。解决办法是在docker-compose里指定用户IDservices:llm-service:user:1000:1000volumes:-/data/models:/app/models:roro表示只读挂载防止容器意外修改模型文件。日志和监控别等出事了才看LLM推理服务的日志量巨大每个请求的输入输出、推理时间、显存占用如果不加控制几天就能把磁盘撑爆。我的做法是使用Docker的日志驱动限制日志文件大小和数量services:llm-service:logging:driver:json-fileoptions:max-size:10mmax-file:3同时在应用代码里只记录关键信息。比如推理时间超过5秒的请求才打日志正常请求只记录一个计数器。另外强烈建议在容器内暴露健康检查接口。Docker的HEALTHCHECK指令可以定期检查服务是否存活HEALTHCHECK --interval30s --timeout10s --start-period60s --retries3 \ CMD curl -f http://localhost:8080/health || exit 1--start-period60s这个参数很重要它告诉Docker给模型加载留出60秒的缓冲时间避免启动阶段误报。多阶段构建把调试和生产分开开发环境和生产环境的需求完全不同。开发时你需要调试工具、测试数据、甚至Jupyter notebook生产环境只需要最小化的运行环境。多阶段构建就是干这个的。看这个例子# 第一阶段开发环境 FROM python:3.10-slim AS dev WORKDIR /app COPY requirements-dev.txt . RUN pip install -r requirements-dev.txt COPY . . CMD [python, -m, debugpy, --listen, 0.0.0.0:5678, src/serve.py] # 第二阶段生产环境 FROM python:3.10-slim AS prod WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY --fromdev /app/src /app/src COPY --fromdev /app/models /app/models USER appuser CMD [python, src/serve.py]构建时指定目标阶段# 开发用dockerbuild--targetdev-tllm-app:dev.# 生产用dockerbuild--targetprod-tllm-app:prod.这样开发镜像里可以装debugpy、pytest、ipython生产镜像里干干净净只保留运行所需的最小依赖。网络配置别让API请求超时LLM推理通常需要GPU而GPU设备在Docker里需要通过--gpus参数暴露。但很多人忽略了一个问题——容器内的CUDA版本和宿主机驱动版本必须匹配。我的经验是使用NVIDIA官方的基础镜像比如nvidia/cuda:12.1-runtime-ubuntu22.04这样CUDA版本和驱动版本都帮你配好了。另外LLM推理的API响应时间通常较长Docker默认的请求超时时间可能不够。在docker-compose里显式设置services:llm-service:ports:-8080:8080environment:-REQUEST_TIMEOUT120deploy:resources:reservations:devices:-driver:nvidiacount:1capabilities:[gpu]那个deploy.resources.reservations.devices字段是Docker Compose v3.8之后支持的新语法比--gpus更灵活。个人经验性建议永远不要在生产环境用:latest标签。我见过太多人图省事结果某天拉了个新镜像模型格式变了服务直接挂掉。用语义化版本号比如v1.2.3每次发布都打tag。模型文件单独管理。别把模型文件放在镜像里用对象存储或者NFS挂载。这样换模型版本时只需要改挂载路径不用重新构建镜像。容器内不要跑多进程。LLM推理本身就很吃资源再跑个监控进程、日志收集进程很容易互相干扰。每个容器只跑一个进程其他功能交给K8s的sidecar模式。启动脚本里加个重试机制。模型加载偶尔会因为显存碎片化失败写个简单的重试逻辑失败后等5秒再试能省去很多半夜被叫醒的痛苦。最后一条也是最重要的——Docker化不是银弹。如果你的LLM应用需要频繁更新模型、或者需要动态调整推理参数考虑用K8s的StatefulSet配合持久化存储别硬塞进Docker里。那次凌晨三点的事故之后我把这套方案写成了内部文档团队里再没人因为Docker部署LLM出过问题。希望这篇笔记也能帮你少踩几个坑。