docker in incus with cuda

容器使用 CUDA

首先是 nvidia-container-toolkit 需要安装。然后 incus launch 时需要加上一些 flag。 常规的包括:

  1. -c security.nesting=true
  2. -c security.syscalls.intercept.mknod=true
  3. -c security.syscalls.intercept.setxattr=true

这几个都是在 incus 里面运行 docker 所需要的,常规操作没有太多变化。而为了 CUDA,还需要以下flag:

  1. -c nvidia.runtime=true,这样就会自动挂载一些 binary 和 library 进去,从而不需要在里面浪费空间再装一次。但是这里有坑,详见 capabilities 配置
  2. -c nvidia.driver.capabilities=compute,utility,video,特别是最后的 video。默认情况下只会设置 compute 和 utility,导致容器内找不到 encoder 的库,用不了硬件加速 codec。

为了最大兼容性,毕竟 ubuntu 等系统的驱动版本可能不一致,导致其他应用不兼容,所以容器的系统我用了跟 host 一样的 archlinux,所以完整的 launch 是:

1
2
3
4
5
6
incus launch images:archlinux/cloud/amd64 dockerd \
-c security.nesting=true \
-c security.syscalls.intercept.mknod=true \
-c security.syscalls.intercept.setxattr=true \
-c nvidia.runtime=true \
-c nvidia.driver.capabilities=compute,utility,video

除了这些flag,还需要把 gpu 添加给容器[1]。使用 physical type 也没有关系,不会导致 container 独占GPU的:

incus config device add dockerd gpu gpu

此时在容器内安装 ffmpeg 并用以下命令测试[2],应该不会提示任何错误的:

1
ffmpeg -loglevel error -f lavfi -i color=black:s=1080x1080 -vframes 1 -an -c:v hevc_nvenc -f null -

docker 配置

容器内依然需要安装 nvidia-container-toolkit,而且需要修改配置禁用 cgroup,否则会报错:

nvidia-container-cli: mount error: failed to add device rules: unable to find any existing device filters attached to the cgroup: bpf_prog_query(BPF_CGROUP_DEVICE) failed: operation not permitted

修改 /etc/nvidia-container-runtime/config.toml 文件,忽略其他内容,把 no-cgroups 设置为 true 即可:

1
2
3
[nvidia-container-cli]
# no-cgroups = false
no-cgroups = true

但是此时用docker启动cuda测试的时候会报错:

nvidia-container-cli: mount error: stat failed: /proc/driver/nvidia/gpus/0000:41:00.0: no such file or directory

这是因为没有把 PCI Bus 挂载到容器里。解决方法就是手动挂载过去,用一个script来方便 host 启动时自动挂载再启动容器,避免 docker 有 restart=always 的容器时无法启动。

挂载 GPU PCI Bus 目录到容器

如果在容器启动前就挂载,启动之后由于host的 nvidia-container-toolkit 会挂载 /proc/driver/nvidia,导致 gpus 目录被隐藏掉。所以需要在容器启动后, docker.service 启动前再挂载。这就是以下这个脚本的逻辑:

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

# 移除可能存在的旧设备(忽略错误)
incus config device remove dockerd gpupcibus || true

# 启动容器
incus start dockerd || { echo "Failed to start container"; exit 1; }

# 添加GPU设备
incus config device add dockerd gpupcibus disk \
source=/proc/driver/nvidia/gpus \
path=/proc/driver/nvidia/gpus || { echo "Failed to add device"; exit 1; }

sleep 3

# 启动容器内的Docker服务
incus exec dockerd -- systemctl start docker || { echo "Failed to start Docker"; exit 1; }

出现下面这个错误的主要原因是 container 还没完全启动,导致内部的 systemd 还没初始化完成,所以会抱怨找不到 bus。

Failed to connect to system scope bus via local transport: No such file or directory

只需要在 script 里面加上 sleep 3incus exec 就可以了。

至于 systemd service 我就不写了。

测试

用 nvidia 的 cuda 容器测试一下:

1
docker run --rm --gpus all nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 nvidia-smi

能够正确输出 GPU 信息,就成功了。

其他有用的信息

Capabilities

参考nvidia的文档:driver capabilities。简单来说:

  • compute: CUDA
  • utility: 使用 nvidia-smi 需要这个
  • video: 视频编解码

其他的通常无关紧要。为什么需要手动指定这三个而不 all,因为 all 会直接让 incus start 报错,启动失败。

快捷控制脚本

把这个放到 /usr/local/bin/dockerd-ctl,就可以方便地启动、停止 docker 容器了。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/bin/zsh

function start() {
# 移除可能存在的旧设备(忽略错误)
incus config device remove dockerd gpupcibus || true

# 启动容器
incus start dockerd || { echo "Failed to start container"; exit 1; }

# 添加GPU设备
incus config device add dockerd gpupcibus disk \
source=/proc/driver/nvidia/gpus \
path=/proc/driver/nvidia/gpus || { echo "Failed to add device"; exit 1; }

sleep 3

# 启动容器内的Docker服务
incus exec dockerd -- systemctl start docker || { echo "Failed to start Docker"; exit 1; }
}

function stop() {
incus stop dockerd
# 移除可能存在的旧设备(忽略错误)
incus config device remove dockerd gpupcibus || true
}

function restart() {
echo "执行 restart 操作..."
stop || { echo "[ERROR] 停止阶段失败"; return 1; }
sleep 1 # 等待资源释放
start || { echo "[ERROR] 启动阶段失败"; return 1; }
echo "✅ 重启完成"
}

### 显示使用说明 ###
function dockerd-ctl() {
cat <<EOF
用法: $0 [command]

可用命令:
start 启动容器并配置GPU
stop 停止容器
restart 重启容器并重新配置GPU

示例:
$0 start
$0 restart
EOF
exit 1
}

### 主逻辑 ###
case "${1:-}" in
start) start ;;
stop) stop ;;
restart) restart ;;
*) dockerd-ctl ;; # 无参数或无效参数时显示用法
esac

exit 0

  1. Enabling GPU passthrough post launch? - simon’s answer ↩︎

  2. test if NVIDIA is available? - nmkd’s answer ↩︎