Docker的学习与使用

参考资料

用 Docker 解决开发环境与生产环境版本不一致的问题,同时避免在不同服务器之间手动同步代码和依赖。本文从基本概念出发,覆盖 Python Web 服务部署和 GPU 模型训练两类实战场景。

1. 基本概念

1.1 镜像(Image)

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

  • 分层存储:镜像构建时一层层叠加,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。构建镜像时,每一层尽量只包含该层需要添加的东西,额外的东西应在该层构建结束前清理掉。

1.2 容器(Container)

镜像和容器的关系,就像面向对象程序设计中的类和实例。镜像是静态定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

1.3 仓库(Repository)

镜像构建完成后可以在宿主机上运行,但若需要在其他服务器上使用,就需要集中存储和分发镜像的服务——Docker Registry。一个 Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。

1.4 安装

https://www.docker.com/

国内拉取镜像较慢时,可配置镜像加速器。编辑 /etc/docker/daemon.json

1
{"registry-mirrors":["http://docker.m.daocloud.io"]}

然后 sudo systemctl restart docker

2. 常用命令

  • 获取镜像:docker pull ubuntu:18.04
  • 交互式运行:docker run -it --rm ubuntu:18.04 bash
    • -i:交互式操作;-t:分配终端
    • --rm:容器退出后自动删除,适合临时调试
    • bash:镜像名后的命令,这里进入 Shell
  • 查看空间占用:docker system df
  • 构建镜像:docker build -t 镜像名:标签 构建上下文路径
  • 进入运行中的容器:docker exec -it 容器名 bash

3. COPY 与 volume 挂载的区别

理解这两者的差异,是避免后面训练容器踩坑的前提。

1
2
3
# Dockerfile — 构建时复制,成为镜像的一部分
COPY app.py .
COPY requirements.txt .
  • COPY:在 docker build 时执行,文件固化在镜像里,与镜像版本绑定。适合:应用代码、依赖声明文件、静态资源。
  • volume(-v:在 docker run 时挂载,文件留在宿主机,可随时修改,容器删除后数据仍保留。适合:日志、上传文件、数据库文件、需要热更新的代码。
1
2
3
4
# docker-compose.yml
volumes:
- ./logs:/app/logs
- ./app:/app

挂载是覆盖关系:宿主机目录会完全遮住容器内同路径下的内容,镜像里原本存在的文件在运行时不可见。这一点在模型训练案例中会导致严重问题,见第 6 节。

4. Python 项目的两种开发模式

方式 操作 特点
容器内开发 启动基础镜像,进容器装依赖、写代码 效率低、环境易丢、依赖混乱
本地开发 + Docker 打包 本地写代码,Dockerfile 构建生产镜像 体验好、可复现、镜像精简

推荐第二种。开发阶段也可以用 volume 挂载代码到容器里调试,兼顾本地编辑体验和容器环境一致性。

5. 案例一:Web API 服务部署

面向需要对外提供 HTTP 接口的 Python 项目(如 FastAPI / uvicorn)。

5.1 开发阶段:Compose 挂载代码

实验环境:macOS,Docker 28.x。

1
2
3
docker pull python:3.11-slim-bullseye
docker-compose up
docker exec -it python-app bash
1
2
3
4
5
6
7
8
9
10
11
services:
app:
image: python:3.11-slim-bullseye
container_name: python-app
working_dir: /app
volumes:
- ./app:/app
ports:
- "8015:8015"
command: tail -f /dev/null
restart: unless-stopped

tail -f /dev/null 让容器在后台持续运行,真正的服务命令通过 docker exec 进入后执行,或在 Compose 里覆盖 command

常用启动参数:

1
2
3
-e PYTHONUNBUFFERED=1        # 实时输出日志
-e PYTHONDONTWRITEBYTECODE=1 # 不生成 .pyc
--name my-python-dev # 指定容器名

5.2 生产阶段:Dockerfile 构建镜像

Dockerfile 是指引 Docker 打包的文档:

指令 作用
FROM 指定基础镜像
WORKDIR 设置工作目录
COPY 复制文件到镜像
RUN 构建时执行的命令
EXPOSE 声明容器监听端口(文档性质)
CMD 容器启动时默认执行的命令
1
2
docker build -t oil-meter-api .
docker run -p 8000:8000 oil-meter-api

-p 8000:8000 是端口映射,格式为 宿主机端口:容器端口,把宿主机的 8000 映射到容器内的 8000。

5.3 Docker Compose 多服务编排

适合 API + 数据库 + 缓存等多容器场景。精简示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: '3.8'
services:
api:
build: .
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
volumes:
- ./logs:/app/logs
depends_on:
- postgres
restart: unless-stopped

postgres:
image: postgres:13-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:

5.4 推送镜像到阿里云

1
2
3
docker login --username=你的用户名 registry.cn-xxx.aliyuncs.com
docker tag [ImageId] registry.cn-xxx.aliyuncs.com/命名空间/仓库:版本号
docker push registry.cn-xxx.aliyuncs.com/命名空间/仓库:版本号

仓库控制台:https://cr.console.aliyun.com/

6. 案例二:GPU 模型训练容器

来自信息抽取(Information Extraction)模型训练场景。与 Web API 不同,训练任务不需要对外暴露端口,因此不用 Docker Compose,而是用 Shell 脚本管理单容器。

6.1 场景与选型

维度 训练容器 Web 服务
用途 GPU 模型训练 对外提供 HTTP API
端口映射 不需要 -p 需要 -p 8000:8000
编排方式 .sh 脚本 Docker Compose
代码同步 运行时 volume 挂载 COPY 进镜像或挂载

核心思路:镜像里固化依赖环境,业务代码通过 volume 挂载,本地改代码容器内即时生效,无需每次重建镜像。

6.2 最终 Dockerfile

虚拟环境放在 /opt/venv,与代码挂载目录分离,避免被 volume 覆盖(详见 6.4 节)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ARG BASE_IMAGE=python:3.12-slim-bookworm
FROM ${BASE_IMAGE}

ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PYTHONUNBUFFERED=1 \
HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# 在临时目录承载依赖同步,避免 .venv 落在会被挂载覆盖的路径
WORKDIR /tmp/build
COPY pyproject.toml uv.lock* ./

RUN apt-get update \
&& apt-get install -y --no-install-recommends libgomp1 \
&& rm -rf /var/lib/apt/lists/* \
&& uv sync --frozen --no-install-project --project /tmp/build \
&& mv /tmp/build/.venv /opt/venv \
&& rm -rf /tmp/build

# 切换回训练和挂载代码的工作目录
WORKDIR /project/information_extraction

ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:${PATH}"

要点:

  • uv 多阶段引入:从官方镜像拷贝 uv / uvx,用 uv sync --frozen 锁定依赖版本。
  • 只 COPY 依赖文件pyproject.toml + uv.lock 进镜像,业务代码不进镜像。
  • **.venv 放在 /opt/venv**:与 /project/information_extraction 分离,挂载代码时不影响虚拟环境。
  • libgomp1:slim 镜像缺少 OpenMP 运行时,PyTorch 等库需要它。
  • 离线变量HF_HUB_OFFLINE=1TRANSFORMERS_OFFLINE=1 防止训练时意外联网拉模型。

构建:

1
sudo docker build -f docker/trainer_dockerfile/Dockerfile -t yzp-trainer:test .

6.3 容器启动脚本

只有一个训练容器,没有多服务编排需求,一个 .sh 足够。

容器启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

# 1. 如果已有同名容器,先清理掉
sudo docker rm -f uie-trainer 2>/dev/null || true

# 2. 启动新容器
sudo docker run -d \
--name uie-trainer \
--gpus "device=1" \
-v "$(pwd)/src/services/information_extraction:/project/information_extraction" \
-w /project/information_extraction \
yzp-trainer:test \
tail -f /dev/null

echo "已启动: sudo docker exec -it uie-trainer bash"

各参数含义:

  • --gpus "device=1":分配宿主机第 2 块 GPU(需安装 NVIDIA Container Toolkit)。
  • -v ...:本地代码目录挂载到容器工作目录,实现代码热同步。
  • tail -f /dev/null:容器后台持续运行,训练命令在 docker exec 进去后执行。
  • 没有 -p:训练不对外提供服务,不需要端口映射。

6.4 经典踩坑:volume 挂载覆盖 .venv

症状:docker build 日志里明明安装了 sentencepieceseqeval 等依赖,但进容器后 uv pip list 只剩孤零零的 pip 25.0.1

原因:-v 挂载是绝对覆盖

1
2
3
Dockerfile 构建时:uv sync 在 /project/information_extraction 下创建了 .venv ✓
容器启动时:-v 宿主机目录:/project/information_extraction 盖住了整个目录 ✗
宿主机目录没有 .venv → 容器里镜像中的 .venv 被隐藏 → uv 退回全局 Python
1
2
3
镜像层:  /project/information_extraction/.venv  ← 构建时装好的依赖
↓ 被 volume 覆盖
运行时: 宿主机目录内容 ← 只有代码,没有 .venv

解决方案:把虚拟环境装在工作目录之外/opt/venv),代码随便挂载,环境岿然不动。

验证流程:

1
2
3
4
5
6
7
8
9
10
# 1. 重新构建(本地有没有 .venv 都不影响)
sudo docker build -f docker/trainer_dockerfile/Dockerfile -t yzp-trainer:test .

# 2. 启动容器
bash run.sh

# 3. 进容器检查
sudo docker exec -it uie-trainer bash
uv pip list # 应列出 pyproject.toml 中的全部依赖
python train.py

6.5 兜底方案:肉身调通 + docker commit 离线固化

即使 Dockerfile 静态构建成功,机器学习场景仍可能有缺漏——某些库在首次运行时会动态下载语言包、权重或额外依赖。开发机还有外网时,可以直接在运行中的容器里把环境彻底跑通,再用 docker commit 固化成最终镜像,带去无网现场。业内戏称这种做法为「肉身调通法」。

第一步:在容器内把环境跑通

1
2
3
4
5
6
7
sudo docker exec -it uie-trainer bash

# 手动补包,或直接跑训练脚本让它自动下载所需资源
uv pip install <缺失的包名>
python trainer.py # 确认训练日志正常、无缺包报错

exit

第二步:commit 固化为新镜像

在宿主机终端执行:

1
2
3
sudo docker ps   # 确认容器 uie-trainer 正在运行

sudo docker commit uie-trainer yzp-trainer-final:v1

关键避坑:docker commit 只保存容器内的文件变化(如 /opt/venv 里新装的包),不会-v 挂载进来的代码和数据打进镜像。这恰好符合「镜像只带环境、代码靠挂载」的设计。

第三步:导出离线安装包

1
sudo docker save -o yzp-trainer_final_v1.tar yzp-trainer-final:v1

yzp-trainer_final_v1.tar 拷贝到无网现场服务器。

第四步:现场加载并启动

run.sh 里把镜像名改为 commit 后的版本:

1
2
yzp-trainer-final:v1 \
tail -f /dev/null

现场执行:

1
2
3
4
sudo docker load -i yzp-trainer_final_v1.tar
bash run.sh
sudo docker exec -it uie-trainer bash
python trainer.py

运行时动态下载的包在「肉身调通」阶段已经写入 /opt/venv,现场拔掉网线也能直接离线训练。

阶段 有网开发机 无网现场
构建 docker build + 容器内补依赖 + docker commit docker load 导入固化镜像
代码 volume 挂载,不进镜像 同样 volume 挂载
环境 /opt/venv 随 commit 固化 直接使用,无需联网

6.6 两个案例的对比

Web API 部署 GPU 训练容器
核心关注 端口映射、多服务编排 GPU 分配、依赖锁定、volume 不覆盖环境
编排 Docker Compose Shell 脚本
依赖管理 requirements.txt / pip uv + pyproject.toml + uv.lock
代码进镜像 生产环境 COPY 进镜像 开发阶段 volume 挂载,不进镜像

2024-06-08 更新

在结合 uv 与 Docker 使用时,最初尝试直接复制 .venv 文件夹至容器内以复用本地 Python 虚拟环境,但发现由于 venv 中部分库采用软链接方式,进入新容器后仍然需要重新下载安装相关依赖。因此,转而选择在容器构建阶段通过 pyproject.toml 管理依赖,使用 uv add 命令将新依赖写入配置文件,uv pip 则不会自动同步到 pyproject.toml,使用带 URL 的依赖时操作也更为繁琐。

针对如 paddlepaddle-gpu 等深度学习框架,需特别注意其与 paddlenlp 等组件之间存在版本耦合关系。推荐的做法是:首先使用 Docker 构建基础镜像阶段完成 Python 依赖安装:

1
2
3
COPY pyproject.toml .
COPY uv.lock .
RUN uv pip install -r uv.lock # 或者 uv pip install -r requirements.txt

随后使用 volume 挂载方式将本地代码文件映射到容器内部:

1
2
3
4
5
sudo docker run -d \
--name uie-trainer \
-v "$(pwd)/src:/project/src" \
镜像名:标签 \
tail -f /dev/null

在有网络的开发或测试环境下,进入容器内部完善环境或下载缺失的资源包后,可通过如下命令将“调整好环境”的容器直接固化为新镜像:

1
sudo docker commit uie-trainer yzp-trainer-final:v1

如需在无网络的部署环境使用该镜像,可先导出为离线包、拷贝至目标服务器,再进行加载与启动:

1
2
3
4
5
6
7
sudo docker save -o yzp-trainer_final_v1.tar yzp-trainer-final:v1
sudo docker load -i yzp-trainer_final_v1.tar
sudo docker run -d \
--name uie-trainer \
-v "/your/host/code:/project/src" \
yzp-trainer-final:v1 \
tail -f /dev/null

待模型训练完成后,可用如下命令将产出的模型或其它成果文件拷贝回宿主机:

1
sudo docker cp uie-trainer:/project/src/your-model-dir ./your-model-dir

后续计划进一步学习如 Kubernetes (K8s) 等容器编排与管理工具,以便实现大规模容器自动化部署与调度。