4、开发环境搭建(二):Docker容器化环境搭建、使用NVIDIA Docker进行GPU加速、Dockerfile编写最佳实践
上一节我们把基础开发环境搞定了。但说实话,搞嵌入式视觉最头疼的就是环境迁移——你在一台机器上配好的OpenCV、TensorRT、CUDA,换台机器就得重来一遍。我早期做项目时,光配环境就花了两天,结果同事的Ubuntu版本不一样,又折腾半天。
Docker就是来解决这个问题的。它把你的整个开发环境打包成一个“集装箱”,拿到哪都能跑。今天我们就聊聊怎么用Docker搭建深度视觉的开发环境,特别是怎么让容器用上GPU。
为什么非得用Docker?
你想想看,一个典型的深度视觉项目依赖什么?CUDA、cuDNN、TensorRT、OpenCV、PyTorch或TensorFlow……这些库的版本稍微不对付,就可能出幺蛾子。Docker把整个环境固化下来,团队里每个人拉同一个镜像,结果就是一致的。
我在项目中遇到过最典型的情况:客户现场需要部署模型,但对方的系统是Ubuntu 20.04,我们开发用的是22.04。如果没有Docker,光是解决CUDA版本兼容问题就够喝一壶的。有了Docker,直接把镜像丢过去,pull下来就能跑。
Docker基础概念速览
如果你之前没用过Docker,这几个概念得先搞明白:
- 镜像(Image): 一个只读的模板,包含运行环境的所有文件。好比一个ISO安装盘。
- 容器(Container): 镜像的运行实例。你可以启动、停止、删除它。好比用ISO装好的操作系统。
- Dockerfile: 一个文本文件,里面写了如何构建镜像的指令。好比安装脚本。
- 仓库(Registry): 存放镜像的地方。最常用的是Docker Hub。
说白了,你写一个Dockerfile,然后build成镜像,最后run成容器。就这么简单。
安装Docker Engine
在Ubuntu上安装Docker,我建议用官方仓库,别用apt自带的版本——太老了。
# 卸载旧版本
sudo apt-get remove docker docker-engine docker.io containerd runc
# 安装依赖
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
# 添加Docker官方GPG密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 设置仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装Docker Engine
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 验证安装
sudo docker run hello-world
看到“Hello from Docker!”就说明装好了。嗯,这里要注意:默认情况下,运行docker命令需要sudo。我习惯把当前用户加到docker组里,省得每次输密码。
sudo usermod -aG docker $USER
# 然后退出重新登录,或者执行 newgrp docker
sudo systemctl restart docker
安装NVIDIA Container Toolkit
做深度视觉,GPU是刚需。但Docker容器默认是访问不到宿主机的GPU的。NVIDIA Container Toolkit就是用来打通这条路的。
我曾经踩过一个坑:在容器里跑PyTorch,结果发现它用的是CPU,训练速度慢得让人抓狂。后来才发现是没装NVIDIA Container Toolkit。
# 设置NVIDIA容器工具包仓库
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
# 安装
sudo apt-get update
sudo apt-get install -y nvidia-docker2
# 重启Docker
sudo systemctl restart docker
# 验证GPU是否可用
sudo docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu22.04 nvidia-smi
如果能看到GPU信息,就说明配置成功了。注意--gpus all这个参数,它告诉Docker把宿主机所有GPU都挂载到容器里。你也可以指定某一块:--gpus '"device=0"'。
sudo apt-get install --only-upgrade libnvidia-container-tools
编写Dockerfile:最佳实践
Dockerfile是构建镜像的蓝图。写得好不好,直接影响镜像大小、构建速度和安全性。我总结了几条经验:
1. 选择合适的基础镜像
别用ubuntu:latest这种大而全的镜像。做深度视觉,我推荐直接用NVIDIA官方提供的CUDA镜像:
# 推荐:带CUDA 11.8和cuDNN 8的Ubuntu 22.04
FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04
# 不推荐:自己从零装CUDA,容易出兼容问题
# FROM ubuntu:22.04
为什么?因为NVIDIA官方镜像已经帮你配好了CUDA和cuDNN的路径,省去很多麻烦。
2. 分层构建,利用缓存
Docker构建镜像时,每一行指令都会生成一个层。如果某层没变,Docker会复用缓存。所以要把变化少的指令放前面:
# 先装系统依赖(变化少)
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
libgl1-mesa-glx \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
# 再装Python包(变化中等)
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# 最后复制代码(变化最频繁)
COPY . /app
这样你改代码后重新构建,只需要重新执行最后两步,前面都走缓存,速度飞快。
3. 合并RUN指令,减少层数
每一条RUN指令都会增加一层。层数太多不仅让镜像变大,还会降低性能。我习惯用&&把多个命令串起来:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-pip \
libgl1-mesa-glx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
注意--no-install-recommends这个参数,它只装核心依赖,不装推荐包,能省不少空间。
4. 使用多阶段构建
如果你的项目需要编译源码(比如编译OpenCV),编译过程中会产生大量临时文件。多阶段构建可以让你只保留最终产物:
# 第一阶段:编译
FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 AS builder
WORKDIR /build
COPY opencv_src/ .
RUN cmake ... && make -j$(nproc)
# 第二阶段:运行
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04
COPY --from=builder /build/install /usr/local
COPY app.py /app/
CMD ["python3", "/app/app.py"]
最终镜像只包含运行时需要的文件,编译工具链全被丢弃了。我有个项目用这个方法,镜像从2.3GB降到了680MB。
5. 不要以root运行
安全起见,容器里最好用非root用户运行应用:
RUN useradd -m -u 1000 appuser
USER appuser
WORKDIR /home/appuser
这样即使容器被攻破,攻击者也拿不到root权限。
完整的Dockerfile示例
下面是一个深度视觉项目的Dockerfile,融合了上面所有最佳实践:
# 使用NVIDIA CUDA 11.8 + cuDNN 8 开发镜像
FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 AS builder
# 避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive
# 安装系统依赖
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-pip \
python3-dev \
build-essential \
cmake \
git \
libgl1-mesa-glx \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender-dev \
libgomp1 \
wget \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# 第二阶段:运行时镜像
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1
# 只复制运行时需要的库
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-pip \
libgl1-mesa-glx \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender-dev \
libgomp1 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 从builder阶段复制Python包
COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 创建非root用户
RUN useradd -m -u 1000 appuser
USER appuser
WORKDIR /home/appuser
# 复制应用代码
COPY --chown=appuser:appuser . .
# 设置入口
CMD ["python3", "main.py"]
构建与运行
写好Dockerfile后,构建镜像:
docker build -t deepvision:1.0 .
运行容器并挂载GPU:
docker run --gpus all -it --rm \
-v $(pwd)/data:/home/appuser/data \
-v $(pwd)/models:/home/appuser/models \
deepvision:1.0
-v参数把宿主机的目录挂载到容器里,这样你修改代码后不需要重新构建镜像,容器里就能看到最新文件。我开发时经常这么干,省去了反复build的时间。
知识体系总览
下面这张图总结了Docker容器化环境搭建的核心流程和关键点:
避坑指南
最后分享几个我踩过的坑:
- 镜像太大: 我第一次构建的镜像有3.5GB,后来发现是没清理apt缓存。加上
rm -rf /var/lib/apt/lists/*后降到1.8GB。 - 时区问题: 容器默认是UTC时间。如果日志需要本地时间,在Dockerfile里加一行:
ENV TZ=Asia/Shanghai。 - 中文乱码: 容器里可能没有中文字体。如果要做图像处理显示中文,记得装字体:
apt-get install fonts-wqy-zenhei。 - 磁盘空间: Docker不会自动清理旧镜像。我习惯定期执行
docker system prune -a,能释放不少空间。
好了,Docker容器化环境搭建就聊到这儿。这套流程我在多个项目里验证过,稳定可靠。你按着步骤来,应该不会出大问题。