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基础概念速览

如果你之前没用过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
小技巧: 加完组后如果还是报权限错误,重启一下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"'

避坑指南: 我曾经在Ubuntu 22.04上装nvidia-docker2时遇到依赖冲突。解决方案是先升级libnvidia-container-tools: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容器化环境搭建的核心流程和关键点:

Docker容器化环境搭建 · 知识体系 ① Docker Engine安装 卸载旧版 → 添加仓库 → 安装 → 验证 sudo docker run hello-world ② NVIDIA GPU加速 安装nvidia-docker2 → 配置运行时 --gpus all 参数挂载GPU ③ Dockerfile最佳实践 • 选择CUDA基础镜像 • 分层构建 + 利用缓存 • 合并RUN指令减少层数 • 多阶段构建 + 非root用户 ④ 构建与运行 docker build -t 镜像名:标签 . docker run --gpus all -it --rm -v 挂载数据卷 -p 端口映射 最终目标:一次构建,到处运行 环境固化 · 团队协作 · 快速部署 · 版本可控

避坑指南

最后分享几个我踩过的坑:

  • 镜像太大: 我第一次构建的镜像有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容器化环境搭建就聊到这儿。这套流程我在多个项目里验证过,稳定可靠。你按着步骤来,应该不会出大问题。