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-slimnvidia/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% 体积
多阶段构建 编译阶段和运行阶段分离 最终镜像只保留运行所需
避坑指南: 我曾经把模型文件(几百 MB)直接 COPY 进镜像,后来发现每次改代码都要重新打包整个模型。正确的做法是:模型文件单独挂载或使用专门的模型仓库,镜像只包含推理代码和轻量依赖。

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" 限制资源,服务就稳定了。

注意: 容器默认没有资源限制,一个容器可能把宿主机所有 CPU 吃光。生产环境一定要加 --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 是编排。把这四步走通,你的模型部署自动化流水线就有了最坚实的底座。