Atlantis
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

4. LXC容器

1. 什么是 LXC?

LXC 全称 Linux Containers,是Linux内核容器特性的用户空间接口,它的历史可以追溯到 2008年,是远比 Docker 古老的容器技术,早期版本 Docker 也是基于 LXC 的,不过后来在 v1.10版本废弃。

使用过 OpenVZ 的朋友可能会对 LXC 倍感亲切,因为 LXC 容器也是一种系统级虚拟化方案,允许用户在单个 Linux 主机上运行多个隔离的用户空间实例,最常见的用法也是替代一些虚拟机的使用场景。

如果将 Docker 容器当作应用容器,那么 LXC 容器就是操作系统级容器。在我看来,LXC 容器最突出的特点就是能在容器内运行完整的 Linux 发行版,提供接近虚拟机的使用体验,同时具备容器的启动速度和运行效率。

但是 LXC 的发展一言难尽,许多人可能都是先接触 Docker 然后再遭遇 LXC,折腾一番后再感叹一句:这是什么???容器里跑一个 initd 吗?

在接触 PVE 前,我主要使用虚拟机和 Docker 容器,因为 LXC 实在不好用,而到了 PVE 上,LXC 容器变成了与 KVM 虚拟机同级别的一等公民。

LXC 容器应不当与 Docker 容器做比较,它的对手是虚拟机。

2. 特权容器

如果只想建一个运行 Docker 的 LXC 容器,那么使用默认配置即可,默认勾选 无特权的容器、嵌套

无特权意味着:

  1. 利用用户命名空间功能,将 LXC 容器内的所有 UID 和 GID 都映射到与主机上不同的数字范围,简单来说,容器内的 root 用户 UID 映射为宿主机上非 root 用户的 UID
  2. 限制容器内的特权操作,如更改网络配置、挂载文件系统、直接访硬件设备等

下图是在宿主机上使用 htop 查看到的特权容器与无特权容器的进程用户 ID:

嵌套则表示允许在 LXC 容器内创建容器,如 LXC 容器和 Docker 容器。

假设我们的使用场景是用 Docker 运行一些 Web 应用、后端服务等,无特权容器已经可以满足需求。

若需要挂载 NFS/SMB 存储,访问宿主机显卡的话,就需要特权容器才能开启对应的权限。

如果遇事不决,没有文档,脑袋发懵,那就选特权容器。

3. 安装 Docker

如上一节所说,最简单最稳妥的办法是创建建一个特权容器,勾选所有可选的功能再安装 Docker:

以 Debian 12 系统为例,直接安装 docker.io 后即可拉取镜像,运行容器:

root@debian:~# apt install docker.io -y
Reading package lists... Done
Building dependency tree... Done
....
root@debian:~# docker version
Client:
 Version:           20.10.24+dfsg1
 API version:       1.41
 Go version:        go1.19.8
 Git commit:        297e128
 Built:             Thu May 18 08:38:34 2023
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server:
 Engine:
  Version:          20.10.24+dfsg1
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.19.8
  Git commit:       5d6db84
  Built:            Thu May 18 08:38:34 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.20~ds1
  GitCommit:        1.6.20~ds1-1+b1
 runc:
  Version:          1.1.5+ds1
  GitCommit:        1.1.5+ds1-1+deb12u1
 docker-init:
  Version:          0.19.0
  GitCommit:
root@debian:~# docker run --rm -it alpine:3.18 sh
Unable to find image 'alpine:3.18' locally
3.18: Pulling from library/alpine
619be1103602: Pull complete 
Digest: sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e5160
Status: Downloaded newer image for alpine:3.18
/ #

除了 Docker 外,Debian 12 版本还自带了 4.x 版本的 Podman,也可以 apt 命令直接安装。

Podman 在 Rootless 容器方面的进度比 Docker 走得更早,我之前在一台公共开发机上就使用 Podman 来代替 Docker。

3. 挂载宿主机目录

LXC 容器与 Docker 容器一样,宿主机的一切皆可挂载,不过截止到当前(2024-05),PVE 的控制台页面仍旧没有暴露挂载宿主机文件目录功能,需要使用命令行操作。

假设需要将宿主机目录 /var/lib/vz 挂载到 ID 为 122 的 LXC 容器内的相同目录,可以运行以下命令:

pct set 122 -mp0 /var/lib/vz,mp=/var/lib/vz
  1. mp0 表示第一个挂载点,如果有 N 个挂载点,就把 0 替换成对应的数字
  2. /var/lib/vz,mp=/var/lib/vz 表示 宿主机目录,mp=LXC容器内目录

执行完后,在宿主机上检查 LXC 容器对应的配置文件,可以发现增加了一行 mp0 开头的配置项:

➜  ~ cat /etc/pve/lxc/122.conf
...
mp0: /var/lib/vz,mp=/var/lib/vz
...

4. 挂载宿主机显卡

现在 LXC 容器内有了 Docker,也有了存储视频的宿主机目录,那么就可以启动一个 jellyfin 来作为影音服务器。

默认情况下 jellyfin 使用 CPU 解码,我们可以挂载宿主机的显卡到 LXC 容器内,然后再将这个显卡挂载给运行 jellyfin 的 Docker 容器。

首先确保宿主机上已经安装好显卡驱动,我的测试显卡是 R5 240,内核自带的 radeon 驱动可以驱动显卡,然后使用 ls -lh 命令检查 /dev/dri 目录:

➜  ~ ls -lh /dev/dri
crw-rw---- 1 root video  226,   0 Apr 17 12:23 card0
crw-rw---- 1 root render 226, 128 Apr 17 12:23 renderD128

我只有一张显卡,这里对应的是 card0 和 renderD128 设备,分别是显卡的显示设备和渲染设备,ls 输出信息中的 226 是设备文件主设备号,0 和 128 是设备文件的次设备号。

假设需要将显卡挂载到 ID 为 122 的 LXC 容器内,编辑配置文件 /etc/pve/lxc/122.conf,在末尾添加以下配置项:

➜  ~ cat /etc/pve/lxc/122.conf
...
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file
lxc.cgroup2.devices.allow: c 226:128 rwm
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file

这里将宿主机的 /dev/dri/card0/dev/dri/renderD128 挂载到了 LXC 容器内的相同位置。

保存并退出后,启动 LXC 容器,进入 LXC 容器中,在运行 jellyfin 的 Docker 容器命令中添加上两个设备即可,注意需要 privileged 权限:

#!/bin/bash

# 镜像名
image="jellyfin:latest"

# 容器名称
container="jellyfin"

# 检查容器是否存在
if [ "$(docker ps -a -q -f name=$container)" ]; then
    # 如果容器存在,则停止容器
    if [ "$(docker ps -q -f name=$container)" ]; then
        docker stop $container
    fi
    # 删除容器
    docker rm $container
fi

# 启动容器: 容器名称-> 重启策略 -> 网络配置 -> 环境变量 -> 挂载项 -> 镜像名
docker run --name $container -d \
    --restart unless-stopped \
    --network host \
    -v $PWD/config:/config \
    -v $PWD/cache:/cache \
    -v $PWD/media:/media \
    -v seagate:/mnt/seagate \
    --device /dev/dri/renderD128:/dev/dri/renderD128 \
    --device /dev/dri/card0:/dev/dri/card0 \
    --privileged=true \
    $image

5. 挂载宿主机tun设备

在 LXC 容器内使用 tailscale 组网时,如果 LXC 容器内不存在 tun 设备的话,只能使用用户空间网络模式,运行效率较低。

这时可以挂载宿主机的 tun 设备来解决这个问题,挂载方式与显卡相同。

在宿主机上使用 ls -lh 命令检查 /dev/net/tun 设备

➜  ~ ls -lh /dev/net/tun
crw-rw-rw- 1 root root 10, 200 May 15 16:02 /dev/net/tun

假设需要将 tun 设备挂载到 ID 为 122 的 LXC 容器内,编辑配置文件 /etc/pve/lxc/122.conf,在末尾添加以下配置项:

➜  ~ cat /etc/pve/lxc/112.conf
...
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file

保存退出后,启动 LXC 容器就可以发现里面增加了刚刚添加的设备,tailscale 也可以正常工作了。