2、Docker容器化基础:Dockerfile编写、镜像构建与优化、容器运行与管理、Docker Compose入门
聊到模型部署,Docker 几乎是绕不开的一环。我刚开始接触时也觉得它有点玄乎,说白了,Docker 就是一个轻量级的「虚拟机」,但它不模拟硬件,而是直接共享宿主机的操作系统内核。你想想看,这意味着什么?意味着你的模型环境可以像快递包裹一样,打包带走,到哪都能跑。
2.1 Dockerfile 编写:从零开始定义你的模型环境
Dockerfile 就是一份「菜谱」。你告诉 Docker 要用什么基础镜像、装什么依赖、复制哪些文件、最后执行什么命令。我习惯把 Dockerfile 放在项目根目录,和代码放在一起,方便版本管理。
来看一个典型的 Python 模型服务 Dockerfile:
# 基础镜像,我一般选 slim 版本,体积小
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 先复制依赖文件,利用 Docker 缓存层
COPY requirements.txt .
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目代码
COPY . .
# 暴露端口
EXPOSE 8080
# 启动命令
CMD ["python", "app.py"]
这里有个小细节:COPY requirements.txt . 和 COPY . . 分开写。为什么?因为 Docker 构建时,每一行指令都会生成一个缓存层。如果先复制依赖文件,只要 requirements.txt 没变,这层缓存就能复用,构建速度能快不少。我在项目中遇到过团队把这两行合并,每次改代码都要重装依赖,白白浪费十几分钟。
python:3.9-slim 或 nvidia/cuda:11.8-runtime。别用 latest 标签,那是个坑,版本不固定,哪天构建出来的镜像可能就变了。
2.2 镜像构建与优化:瘦身才是硬道理
镜像太大,传输慢、启动慢、还占磁盘。我见过有人把模型文件、训练代码、测试数据全塞进一个镜像,结果 5 个 G。部署到生产环境,光拉取镜像就要等半天。
优化的核心思路就两条:减少层数 和 清理无用文件。
举个例子,如果你需要编译一些 C 扩展,可以这样写:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libc6-dev && \
pip install some-package-with-c-ext && \
apt-get purge -y gcc libc6-dev && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
看到了吗?安装、编译、卸载,全放在一个 RUN 里。这样只生成一层,而且编译工具用完后立刻删掉,镜像体积能小 30% 以上。
| 优化手段 | 说明 | 效果 |
|---|---|---|
| 使用 .dockerignore | 排除本地无关文件(如 .git、__pycache__) | 减少上下文体积 |
| 合并 RUN 指令 | 用 && 连接多条命令 | 减少镜像层数 |
| 选择 slim/alpine 基础镜像 | 体积更小,攻击面更少 | 减少 50%-80% 体积 |
| 多阶段构建 | 编译阶段和运行阶段分离 | 最终镜像只保留运行所需 |
2.3 容器运行与管理:让模型跑起来
镜像构建好了,接下来就是运行容器。最基本的命令:
# 运行一个容器
docker run -d --name my-model -p 8080:8080 my-model-image:latest
# 查看运行中的容器
docker ps
# 查看日志
docker logs -f my-model
# 进入容器内部
docker exec -it my-model /bin/bash
# 停止并删除容器
docker stop my-model && docker rm my-model
-d 是后台运行,-p 是端口映射。你想想看,宿主机 8080 端口映射到容器内 8080 端口,外部请求就能直接打到模型服务了。
管理容器时,我常用 docker stats 查看资源占用。有一次线上模型响应变慢,我一看 docker stats,CPU 飙到 90%,内存也快满了。后来加了 --memory="4g" --cpus="2" 限制资源,服务就稳定了。
--memory 和 --cpus 限制。
2.4 Docker Compose 入门:一键启动多服务
模型部署往往不止一个服务。比如你有一个推理 API,一个 Redis 做缓存,一个 Nginx 做反向代理。一个个手动启动太麻烦,Docker Compose 就是干这个的。
一个典型的 docker-compose.yml:
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: always
model-api:
build: ./model
ports:
- "8080:8080"
depends_on:
- redis
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- model-api
restart: always
然后一行命令搞定:
docker-compose up -d
它会自动构建镜像、创建网络、按依赖顺序启动服务。我特别喜欢 depends_on 这个配置,它确保 Redis 先启动,模型 API 再启动,不会出现连接失败的情况。
嗯,这里要注意:depends_on 只控制启动顺序,不等待服务完全就绪。如果 Redis 启动慢,模型 API 可能还是会连不上。我一般会在代码里加个重试逻辑,或者用 healthcheck 来确保服务真正可用。
docker-compose up,生产环境建议用 docker stack deploy 或 Kubernetes。Compose 更适合单机多容器场景,集群部署还是得上编排工具。
好了,Docker 容器化基础就聊到这。说白了,Dockerfile 是蓝图,镜像构建是打包,容器运行是交付,Compose 是编排。把这四步走通,你的模型部署自动化流水线就有了最坚实的底座。