Docker基础:镜像、容器、Dockerfile、常用命令

核心原理:镜像与容器的关系

在模型训练管道中,我们最常遇到的困惑是“镜像”和“容器”到底有什么区别。简单来说,镜像(Image)是一个只读的、静态的文件系统模板,它包含了运行某个应用所需的全部依赖——Python解释器、CUDA驱动、PyTorch库、训练脚本等。而容器(Container)是镜像的运行实例,它拥有自己的文件系统读写层、网络栈和进程空间。你可以把镜像想象成一张光盘,容器则是用这张光盘启动的一台虚拟机——光盘本身不会变,但虚拟机里可以写入临时文件。

这种分层设计对模型训练至关重要。当我们基于 pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime 镜像启动容器时,所有训练产生的模型权重、日志文件都写入容器的可写层。一旦容器被删除,这些数据也随之消失——除非我们使用卷(Volume)将数据持久化到宿主机。理解这一机制,是后续设计训练管道的第一步。

镜像 Image 只读模板 CUDA 12.1 + cuDNN Python 3.10 + PyTorch 训练脚本 + 依赖包 只读层(不可修改) docker run 容器 Container 运行实例 进程:python train.py 可写层(临时文件) 镜像层(只读引用) 卷挂载:/data → 宿主机 宿主机存储 模型权重 / 日志 数据集 持久化 rm 容器后 可写层数据丢失

方法流程:构建训练镜像的Dockerfile

编写Dockerfile是Docker化的核心工作。一个典型的训练镜像Dockerfile包含以下关键指令,我们以PyTorch GPU训练环境为例:

# 1. 指定基础镜像(必须包含CUDA驱动)
FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime

# 2. 设置工作目录
WORKDIR /workspace

# 3. 复制依赖清单并安装(利用缓存层)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 4. 复制训练代码
COPY train.py .
COPY models/ ./models/

# 5. 声明容器运行时监听的端口(可选)
EXPOSE 6006

# 6. 设置默认启动命令
ENTRYPOINT ["python", "train.py"]

构建命令为 docker build -t my-trainer:v1 .,其中 -t 指定镜像名称和标签。这里有一个重要的缓存策略:将依赖安装步骤放在代码复制之前。因为Docker会逐层缓存,只要 requirements.txt 未变化,后续构建就会直接复用“安装依赖”这一层,大幅缩短迭代时间。在模型训练场景中,我们经常需要频繁修改训练脚本,但依赖很少变动,这个顺序能显著提升开发效率。

案例场景:训练容器的生命周期管理

假设我们要运行一个分布式训练任务,使用 train.py 脚本,需要挂载数据集并保存模型。以下是完整的命令流程:

# 1. 构建镜像
docker build -t trainer:latest .

# 2. 查看本地镜像
docker images

# 3. 启动容器(前台运行,便于查看日志)
docker run --gpus all \
  --name resnet50-job \
  -v /mnt/data:/workspace/data \
  -v /mnt/output:/workspace/output \
  trainer:latest

# 4. 另开终端查看容器进程
docker ps

# 5. 训练完成后停止并删除容器
docker stop resnet50-job
docker rm resnet50-job

这里 --gpus all 将宿主机GPU暴露给容器,-v 实现目录挂载。训练结束后,模型权重保存在宿主机的 /mnt/output 目录中,即使容器被删除也不会丢失。如果训练中断,我们可以用 docker start resnet50-job 重新启动容器,但要注意:容器内的进程状态不会自动恢复,需要训练脚本本身支持断点续训(例如读取checkpoint)。

常用命令速查表

命令 用途 训练场景示例
docker build 构建镜像 docker build -t trainer:v2 .
docker run 启动容器 docker run --gpus all -v /data:/data trainer:v2
docker ps -a 查看所有容器 检查训练任务是否退出
docker logs 查看容器日志 docker logs -f resnet50-job
docker exec 进入运行中容器 docker exec -it resnet50-job bash
docker cp 复制文件 docker cp resnet50-job:/workspace/log.txt ./
docker commit 将容器保存为新镜像 保存调试后的环境

误区与复盘:常见陷阱

误区1:在容器内使用 pip install 后不提交镜像。 容器一旦删除,所有手动安装的包都会丢失。正确做法是修改Dockerfile并重新构建,或者使用 docker commit 将当前容器保存为新镜像。

误区2:忽略 .dockerignore 文件。 如果不排除 __pycache__、.git、大型数据集等目录,docker build 会把它们全部发送到Docker守护进程,导致构建极慢且镜像臃肿。

误区3:以交互方式运行训练任务。 使用 docker run -it 启动训练脚本,一旦SSH断开,容器收到SIGHUP信号就会终止。生产环境应使用 docker run -d 后台运行,或配合 nohup 和 tmux。

复盘建议: 每次训练任务结束后,执行 docker system df 查看磁盘占用,及时清理悬空镜像(docker image prune)和已停止的容器(docker container prune)。在团队协作中,将Dockerfile纳入版本控制,并固定基础镜像的标签(不要使用 latest),确保每次构建的可复现性。