Dockerfile 如何构建一个 Python 算法镜像?
码海寻道 · AI 算法工程化与系统落地实战第 3 篇
上一篇已经把算法封装成 FastAPI 服务,但它仍然依赖当前机器的 Python、系统库、模型路径和启动命令。Dockerfile 的作用,就是把这些运行条件固化成一个可以重复构建的镜像。
本文以一个 FastAPI 算法服务为例,说明 Dockerfile 怎么写、镜像怎么构建、模型文件放在哪里,以及为什么"能构建成功"还不等于"可以上线"。
一、先理解镜像和容器
text
Dockerfile:构建镜像的说明书
镜像:应用代码、运行时和依赖的只读模板
容器:镜像启动后的运行实例
镜像应该尽量包含稳定的运行环境和应用代码;数据库、上传文件、日志和可变业务数据不应该依赖容器可写层。容器被删除后,业务数据仍然要能从 Volume、对象存储或外部数据库恢复。
二、准备项目目录
text
algorithm-api/
├── app/
│ ├── main.py
│ ├── config.py
│ └── inference.py
├── requirements.txt
├── Dockerfile
└── .dockerignore
依赖文件示例:
text
fastapi
uvicorn[standard]
pydantic-settings
如果模型运行时需要 onnxruntime、opencv-python-headless 或 PyTorch,应该根据 CPU/GPU 环境分别锁定依赖,不要在同一镜像中无条件安装两套大型运行时。
三、一个可用的基础 Dockerfile
dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip install -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]
这里有一个重要顺序:先复制依赖文件并安装,再复制业务代码。这样业务代码修改时可以复用依赖安装层,缩短构建时间。
FastAPI 官方文档目前建议根据实际部署环境自行构建镜像,使用 fastapi run 或适合当前部署方式的 Worker 配置,不要继续依赖已经弃用的通用 FastAPI 基础镜像。FastAPI 容器部署文档
四、为什么要写 .dockerignore?
没有 .dockerignore,构建上下文可能把以下内容一起发送给 Docker:
text
.git/
.venv/
__pycache__/
.env
tests/data/
模型临时文件
日志和调试输出
建议:
text
.git
.venv
__pycache__
*.pyc
.env
.pytest_cache
logs
data/tmp
.env 尤其重要,它可能包含数据库密码、模型供应商 Token 或对象存储密钥。忽略文件只能减少误打包,不能替代密钥管理;已经提交到 Git 的密钥仍需要轮换。
五、模型文件要不要打进镜像?
通常有三种方案:
| 方案 | 优点 | 风险 |
|---|---|---|
| 打进镜像 | 启动简单,版本绑定 | 镜像很大,更新慢 |
| 启动时下载 | 镜像小 | 启动依赖网络和凭据 |
| 挂载模型目录 | 更新灵活 | 需要管理路径、权限和版本 |
选择时看模型大小、更新频率和部署环境。无论采用哪种方案,都应该记录模型版本和校验和:
text
model_name=classifier
model_version=v3
sha256=...
不要让容器启动时无条件下载一个"最新模型"。这会让同一个镜像在不同时间启动出不同结果,也会让故障恢复依赖外部网络。
六、把模型目录作为 Volume 挂载
开发环境可以这样运行:
bash
docker run --rm \
-p 8000:8000 \
-v "${PWD}/models:/app/models:ro" \
-e ALGO_MODEL_PATH=/app/models/classifier-v3.onnx \
algorithm-api:dev
注意使用只读挂载 :ro。算法服务通常只需要读取模型,不应该有权限修改模型文件。生产环境还要限制模型目录来源,并在启动时检查文件权限和哈希。
七、用非 root 用户运行
默认 root 用户运行会扩大容器内漏洞的影响范围。可以在镜像中创建普通用户:
dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN groupadd --system app \
&& useradd --system --gid app --create-home app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app app ./app
USER app
EXPOSE 8000
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]
如果运行时需要写临时文件,应明确创建一个可写目录,而不是让整个根文件系统保持可写。
八、添加健康检查
健康检查应该检查服务是否真的能接收请求,而不只是进程存在:
dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"
应用中提供轻量接口:
python
@app.get("/health")
def health():
return {"status": "ok"}
如果模型尚未加载完成,建议另外提供 /ready,返回未就绪状态,而不是让健康检查把正在加载模型的容器反复重启。
九、构建、运行和排查
构建镜像:
bash
docker build -t algorithm-api:0.1.0 .
运行:
bash
docker run --rm -p 8000:8000 algorithm-api:0.1.0
查看日志和进入容器:
bash
docker logs -f <container_id>
docker exec -it <container_id> sh
常见问题:
- 容器启动即退出:检查
CMD、模块路径和启动日志; - 找不到模型:检查容器内路径,不要使用宿主机绝对路径;
- 端口无法访问:确认应用监听
0.0.0.0,且端口映射正确; - 构建特别慢:检查
.dockerignore和依赖缓存; - 镜像体积过大:拆分 CPU/GPU 依赖,删除构建缓存和无用文件。
十、生产构建还要关注什么?
正式发布前建议补充:
- 固定基础镜像和依赖版本;
- 生成依赖清单和漏洞扫描报告;
- 使用 Git SHA 或不可变 Digest 标记镜像;
- 不把 API Key 和数据库密码写进镜像;
- 设定 CPU、内存、GPU 和临时目录限制;
- 记录镜像、模型和配置版本;
- 准备回滚镜像和兼容的数据库迁移方案。
镜像只是交付单位,不是完整部署方案。下一篇将介绍如何用 Docker Compose 把算法 API、数据库和缓存组合起来。
参考资料
本文为"码海寻道"原创技术文章。基础镜像、Python 版本和推理运行时应根据目标 CPU/GPU 环境进行压测和兼容性验证。
