Docker的学习与使用
Docker的学习与使用
用 Docker 解决开发环境与生产环境版本不一致的问题,同时避免在不同服务器之间手动同步代码和依赖。本文从基本概念出发,覆盖 Python Web 服务部署和 GPU 模型训练两类实战场景。
1. 基本概念
1.1 镜像(Image)
Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
- 分层存储:镜像构建时一层层叠加,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。构建镜像时,每一层尽量只包含该层需要添加的东西,额外的东西应在该层构建结束前清理掉。
1.2 容器(Container)
镜像和容器的关系,就像面向对象程序设计中的类和实例。镜像是静态定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
1.3 仓库(Repository)
镜像构建完成后可以在宿主机上运行,但若需要在其他服务器上使用,就需要集中存储和分发镜像的服务——Docker Registry。一个 Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。
1.4 安装
国内拉取镜像较慢时,可配置镜像加速器。编辑 /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 | # Dockerfile — 构建时复制,成为镜像的一部分 |
- COPY:在
docker build时执行,文件固化在镜像里,与镜像版本绑定。适合:应用代码、依赖声明文件、静态资源。 - volume(
-v):在docker run时挂载,文件留在宿主机,可随时修改,容器删除后数据仍保留。适合:日志、上传文件、数据库文件、需要热更新的代码。
1 | # docker-compose.yml |
挂载是覆盖关系:宿主机目录会完全遮住容器内同路径下的内容,镜像里原本存在的文件在运行时不可见。这一点在模型训练案例中会导致严重问题,见第 6 节。
4. Python 项目的两种开发模式
| 方式 | 操作 | 特点 |
|---|---|---|
| 容器内开发 | 启动基础镜像,进容器装依赖、写代码 | 效率低、环境易丢、依赖混乱 |
| 本地开发 + Docker 打包 | 本地写代码,Dockerfile 构建生产镜像 | 体验好、可复现、镜像精简 |
推荐第二种。开发阶段也可以用 volume 挂载代码到容器里调试,兼顾本地编辑体验和容器环境一致性。
5. 案例一:Web API 服务部署
面向需要对外提供 HTTP 接口的 Python 项目(如 FastAPI / uvicorn)。
5.1 开发阶段:Compose 挂载代码
实验环境:macOS,Docker 28.x。
1 | docker pull python:3.11-slim-bullseye |
1 | services: |
tail -f /dev/null 让容器在后台持续运行,真正的服务命令通过 docker exec 进入后执行,或在 Compose 里覆盖 command。
常用启动参数:
1 | -e PYTHONUNBUFFERED=1 # 实时输出日志 |
5.2 生产阶段:Dockerfile 构建镜像
Dockerfile 是指引 Docker 打包的文档:
| 指令 | 作用 |
|---|---|
FROM |
指定基础镜像 |
WORKDIR |
设置工作目录 |
COPY |
复制文件到镜像 |
RUN |
构建时执行的命令 |
EXPOSE |
声明容器监听端口(文档性质) |
CMD |
容器启动时默认执行的命令 |
1 | docker build -t oil-meter-api . |
-p 8000:8000 是端口映射,格式为 宿主机端口:容器端口,把宿主机的 8000 映射到容器内的 8000。
5.3 Docker Compose 多服务编排
适合 API + 数据库 + 缓存等多容器场景。精简示例如下:
1 | version: '3.8' |
5.4 推送镜像到阿里云
1 | docker login --username=你的用户名 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 | ARG BASE_IMAGE=python:3.12-slim-bookworm |
要点:
- uv 多阶段引入:从官方镜像拷贝
uv/uvx,用uv sync --frozen锁定依赖版本。 - 只 COPY 依赖文件:
pyproject.toml+uv.lock进镜像,业务代码不进镜像。 - **
.venv放在/opt/venv**:与/project/information_extraction分离,挂载代码时不影响虚拟环境。 - libgomp1:slim 镜像缺少 OpenMP 运行时,PyTorch 等库需要它。
- 离线变量:
HF_HUB_OFFLINE=1和TRANSFORMERS_OFFLINE=1防止训练时意外联网拉模型。
构建:
1 | sudo docker build -f docker/trainer_dockerfile/Dockerfile -t yzp-trainer:test . |
6.3 容器启动脚本
只有一个训练容器,没有多服务编排需求,一个 .sh 足够。

1 |
|
各参数含义:
--gpus "device=1":分配宿主机第 2 块 GPU(需安装 NVIDIA Container Toolkit)。-v ...:本地代码目录挂载到容器工作目录,实现代码热同步。tail -f /dev/null:容器后台持续运行,训练命令在docker exec进去后执行。- 没有
-p:训练不对外提供服务,不需要端口映射。
6.4 经典踩坑:volume 挂载覆盖 .venv
症状:docker build 日志里明明安装了 sentencepiece、seqeval 等依赖,但进容器后 uv pip list 只剩孤零零的 pip 25.0.1。
原因:-v 挂载是绝对覆盖。
1 | Dockerfile 构建时:uv sync 在 /project/information_extraction 下创建了 .venv ✓ |
1 | 镜像层: /project/information_extraction/.venv ← 构建时装好的依赖 |
解决方案:把虚拟环境装在工作目录之外(/opt/venv),代码随便挂载,环境岿然不动。
验证流程:
1 | # 1. 重新构建(本地有没有 .venv 都不影响) |
6.5 兜底方案:肉身调通 + docker commit 离线固化
即使 Dockerfile 静态构建成功,机器学习场景仍可能有缺漏——某些库在首次运行时会动态下载语言包、权重或额外依赖。开发机还有外网时,可以直接在运行中的容器里把环境彻底跑通,再用 docker commit 固化成最终镜像,带去无网现场。业内戏称这种做法为「肉身调通法」。
第一步:在容器内把环境跑通
1 | sudo docker exec -it uie-trainer bash |
第二步:commit 固化为新镜像
在宿主机终端执行:
1 | sudo docker ps # 确认容器 uie-trainer 正在运行 |
关键避坑: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 | yzp-trainer-final:v1 \ |
现场执行:
1 | sudo docker load -i yzp-trainer_final_v1.tar |
运行时动态下载的包在「肉身调通」阶段已经写入 /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 | COPY pyproject.toml . |
随后使用 volume 挂载方式将本地代码文件映射到容器内部:
1 | sudo docker run -d \ |
在有网络的开发或测试环境下,进入容器内部完善环境或下载缺失的资源包后,可通过如下命令将“调整好环境”的容器直接固化为新镜像:
1 | sudo docker commit uie-trainer yzp-trainer-final:v1 |
如需在无网络的部署环境使用该镜像,可先导出为离线包、拷贝至目标服务器,再进行加载与启动:
1 | sudo docker save -o yzp-trainer_final_v1.tar yzp-trainer-final:v1 |
待模型训练完成后,可用如下命令将产出的模型或其它成果文件拷贝回宿主机:
1 | sudo docker cp uie-trainer:/project/src/your-model-dir ./your-model-dir |
后续计划进一步学习如 Kubernetes (K8s) 等容器编排与管理工具,以便实现大规模容器自动化部署与调度。



