
Rust AI CLI 的 Docker 化部署把工具和依赖模型一起打包标签AI、AI CLI、Rust、Docker、部署一、从一次在我机器上能跑说起前阵子我做了一个 Rust 写的小命令行工具功能是读取一段本地代码然后调用本地大模型Ollama对代码做审查。工具本身不复杂代码量也就一千多行。自己在 Mac 上跑得好好的结果发给一个朋友试用对方折腾了半小时愣是没跑起来。问题出在哪不是代码写得烂是环境不一致。我下意识觉得对方和我一样本地装了 Ollama、拉好了模型、配好了 Rust 环境。但实际情况是他没装 Ollama模型当然也没有连 Rust 工具链都是两个月前装的版本对不上。那一刻我突然理解了为什么 Docker 这种东西会存在。这篇文章就记录一下我是怎么把一个 Rust AI CLI 工具连同模型依赖一起打包进 Docker 镜像的。不是什么高深的东西就是我踩坑之后的一点整理。二、理清依赖关系到底要打包什么动手之前先把自己工具的依赖链条画清楚。很多时候部署出问题不是 Dockerfile 写错了是没搞清楚自己的工具到底依赖了哪些东西。flowchart TD A[Rust AI CLI 工具] -- B[系统依赖] A -- C[运行时依赖] A -- D[AI 模型依赖] B -- B1[glibc / musl] B -- B2[OpenSSL / 网络库] B -- B3[CA 证书] C -- C1[本地模型服务 (Ollama)] C -- C2[模型权重文件] D -- D1[qwen2.5-coder:7b] D -- D2[模型已在 Ollama 注册] E[用户环境] -.-|问题不一致| B1 E -.-|问题可能没有| C1 E -.-|问题需手动拉取| D1图上很清晰依赖分三层。系统依赖glibc、OpenSSL 这些运行时依赖Ollama 服务模型依赖具体的模型文件。之前我只打包了 Rust 编译出来的二进制文件那只是冰山一角。真正的部署得把上面三层全部搞定。三、多阶段构建把镜像做小Docker 多阶段构建是我后来才学会的技巧。一开始我是直接在镜像里装 Rust 编译器结果镜像大小直奔 2GB。后来改成两阶段构建瘦身效果很明显。下面是我最终用的 Dockerfile核心思路是编译阶段用完整的 Rust 镜像运行阶段用 Alpine 的精简镜像。# 第一阶段编译 Rust 程序 # 使用官方 Rust 镜像作为编译环境 FROM rust:1.78-alpine AS builder # 安装编译所需的系统依赖 # Alpine 用的是 musl libc不是 glibc静态编译更省事 RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig # 设置工作目录 WORKDIR /app # 先复制 Cargo.toml 和 Cargo.lock利用 Docker 缓存层 # 这样只有依赖变化时才重新下载日常改代码不会每次都下依赖 COPY Cargo.toml Cargo.lock ./ # 创建一个空的 src/main.rs 先骗过 cargo build # 这一步只编译依赖不编译业务代码 RUN mkdir src echo fn main() {} src/main.rs RUN cargo build --release rm -rf src # 现在复制真正的源代码并编译 COPY src ./src # 编译最终二进制使用静态链接避免运行时缺 .so 文件 RUN cargo build --release --target x86_64-unknown-linux-musl # 第二阶段运行环境 # 使用 Alpine 作为最小的基础镜像 FROM alpine:3.20 # 安装运行时需要的基础库 # ca-certificatesHTTPS 请求需要 # curl用于拉取和初始化 Ollama RUN apk add --no-cache ca-certificates curl bash # 安装 Ollama轻量级单二进制文件 # 版本需要与本地测试环境保持一致 RUN curl -fsSL https://ollama.com/install.sh | sh # 从编译阶段复制编译好的二进制 COPY --frombuilder /app/target/x86_64-unknown-linux-musl/release/code-review-cli /usr/local/bin/code-review-cli # 创建非 root 用户运行安全的好习惯 RUN adduser -D -h /home/appuser appuser USER appuser WORKDIR /home/appuser # 暴露 Ollama 的默认端口如果需要外部访问 EXPOSE 11434 # 启动脚本先启动 Ollama 服务拉取模型然后保持运行 COPY entrypoint.sh /usr/local/bin/entrypoint.sh ENTRYPOINT [entrypoint.sh]对应的entrypoint.sh启动脚本#!/bin/bash # 启动脚本负责启动 Ollama 服务并拉取模型 set -e # 任何命令失败就退出 echo 启动 Ollama 服务 # 后台启动 Ollama 服务 ollama serve # 获取 Ollama 进程的 PID方便后续管理 OLLAMA_PID$! echo 等待 Ollama 服务就绪 # 轮询等待 Ollama API 可用最多等 30 秒 for i in $(seq 1 30); do if curl -s http://localhost:11434/api/tags /dev/null 21; then echo Ollama 服务已就绪 break fi echo 等待中... ($i/30) sleep 1 done echo 检查并拉取模型 # 如果模型不存在就拉取model_name 可从环境变量传入 MODEL${AI_MODEL:-qwen2.5-coder:7b} if ! ollama list | grep -q $MODEL; then echo 模型 $MODEL 不存在开始拉取... ollama pull $MODEL echo 模型拉取完成 else echo 模型 $MODEL 已存在跳过拉取 fi echo 工具就绪 # 把 Ollama PID 传给前台保持容器运行 wait $OLLAMA_PID这里有几个我在踩坑过程中学到的细节Alpine musl用 glibc 编译的二进制在 Alpine 上跑不了用 musl 做静态链接就解决了。层缓存先复制 Cargo.toml 再复制 src这样只有依赖变化时才重新编译依赖日常开发改代码时编译飞快。非 root 用户容器里用 root 跑是很多安全问题的根源养成习惯切换成普通用户。四、模型层缓存策略避免每次拉几GB模型模型文件动不动几个 G如果你的 CI 每次都重新拉模型不说时间流量费都够呛。我用的办法是用 Docker 的多阶段缓存把模型单独放一层# 模型准备层可缓存 FROM alpine:3.20 AS model-fetcher # 安装 Ollama RUN curl -fsSL https://ollama.com/install.sh | sh # 启动 Ollama 在后台拉取指定模型然后停止 # 这样做的好处是模型层的哈希只取决于模型名称和版本 RUN ollama serve \ sleep 5 \ ollama pull qwen2.5-coder:7b \ kill %1 # 最终镜像层 FROM alpine:3.20 # ... 安装运行时依赖 ... # 从模型层复制 Ollama 的数据目录包含已下载的模型 COPY --frommodel-fetcher /root/.ollama /home/appuser/.ollama # 从编译层复制二进制 COPY --frombuilder /app/target/release/code-review-cli /usr/local/bin/ # 启动 Ollama 即可使用已有模型无需重新拉取 CMD [ollama, serve]这样子只有模型版本变了模型层才会重新构建。日常改代码的构建时间从等着拉 4GB 模型变成了秒级编译。我画个图总结一下整个镜像的分层结构graph TB subgraph 最终镜像约 5-8 GB direction TB L1[层 1Alpine 基础系统 (~5MB)] L2[层 2运行时依赖 (curl, bash, ca-cert) (~10MB)] L3[层 3Ollama 二进制 (~800MB, 可缓存)] L4[层 4AI 模型文件 (~4GB, 可缓存)] L5[层 5Rust 编译产物 (~10MB, 频繁更新)] L6[层 6启动脚本 配置 (~1KB)] end Build1[改代码 → 重建] -.-|只重建层 5,6| L5 Build2[改模型 → 重建] -.-|重建层 4,5,6| L4 Build3[改 Ollama 版本] -.-|重建层 3,4,5,6| L3 style L1 fill:#e8f5e9 style L2 fill:#e8f5e9 style L3 fill:#fff3e0 style L4 fill:#ffebee style L5 fill:#e3f2fd style L6 fill:#f3e5f5五、总结回顾整个折腾过程几点我觉得对我这种新手来说比较重要的认知能跑和能交付是两码事。自己机器上能跑不算数别人能一键跑起来的才是能交付的。Docker 不是银弹但对于环境一致性确实是好工具。以前我觉得 Docker 是运维才需要学的东西现在发现不管写什么工具能打包成镜像就方便太多。多阶段构建是基础操作不是高级技巧。它能把编译依赖和运行环境分离镜像大小能差出几十倍。模型层的缓存很重要。AI 工具部署的特殊性在于模型文件巨大必须单独处理缓存策略。Alpine musl 的组合对于 Rust 的静态编译非常友好基本不用操心 .so 文件缺失的问题。我还是个新手上面这些都是踩过坑才学到的。如果你也在做类似的事情希望这些经验能帮你少走点弯路。有什么说得不对的地方也欢迎指正。