一步步教你在 Ubuntu 24.04 上搭建 LXD 容器并配置 ZFS 存储,涵盖安装、VPS 与本地 ZFS 存储池创建、容器启动、镜像发布及 SSH 连接配置。
简介
随着服务器功能的发展,它们越来越多地被用作开发环境 —— 尤其是基于 Linux 的工具。在 Windows 上安装 Coq 等语言可能较为困难,而快速原型开发通常会留下杂乱的依赖项,事后很难彻底清理。像 Vim 这类支持 LSP 的工具,也需要多个步骤才能配置和卸载。LXD 提供了轻量级的、操作系统级别的容器,支持隔离的多用户环境。它允许用户共享通用文件(例如 Vim 配置),同时为每个容器提供完全隔离的环境。与 Docker 类似,你可以发布自己的镜像,并根据需要进一步更新它。
另一个使用场景是在实验室环境中,当像 GPU 这样的硬件资源有限时,你可能希望每个组员都能使用该资源,而不会互相干扰。在这种情况下,你也可以使用 LXD 为每个用户创建一个容器,共享相同的硬件资源。此外,你还可以挂载公共文件(如数据集)来减少内存使用。
如何通过 Snap 在 Ubuntu 上安装 LXD
只需复制以下命令粘贴到终端中即可:
snap install lxd
Ubuntu 已经预装了 snap,如果你使用的是其他系统,请先使用你的包管理器安装 snap。
安装成功后,你应该能看到如下提示信息:
lxd (5.21/stable) 5.21.3-c5ae129 from Canonical installed
如果你不是使用 root 账户,并且不希望每次使用 lxd 或 lxc 时都输入 sudo,可以将当前用户添加到 lxd 用户组中:
sudo usermod -aG lxd $USER
要使更改生效,可以选择注销并重新登录,或执行以下命令:
newgrp lxd
🔧 避免以 root 身份运行 lxd 或 lxc
一个实用建议:不要用 root 来运行 lxd 或 lxc。即使你已经在 lxd 用户组中,直接使用 sudo 或以 root 身份运行这些命令仍可能带来问题,因为有些情况下只需要特定权限而不是完全的 root 权限。
比如,如果你直接用 root 来执行命令,可能会看到这样的错误:
permanently dropping privs did not work: File exists
为 LXD 配置 ZFS 存储池(VPS 与本地)
如果你在 VPS 上部署服务,内存通常是有限的。因此,我希望能更清晰、更高效地进行管理。我选择了 ZFS 作为存储后端,原因是它在复制容器时只保存增量修改,能显著减少冗余、节省空间。同时你也可以手动定义存储池,更方便备份及指定数据的物理位置。
先执行以下命令安装:
apt install zfsutils-linux -y
如果没有报错,那就说明安装成功了。你可以再确认一下版本信息:
zfs --version
🖥️ 在本地机器上创建 ZFS 存储池
⏩ 如果你用的是 VPS,可以直接跳到 ☁️ 在 VPS 上通过虚拟磁盘创建 ZFS 存储池 部分。
如果你在实验室用的是一台个人电脑,可以直接指定你想用的物理磁盘,比如:
zpool create pool /dev/sda
这条命令的意思是,在 /dev/sda 这个磁盘上创建一个名为 pool 的 ZFS 存储池。
如果你看到类似这样的错误:
cannot resolve path '/dev/sda'
说明你的机器上并没有 /dev/sda 这个磁盘。先用下面的命令确认你实际的磁盘设备名:
df /
输出可能类似这样:
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda 62328864 10646208 48947556 18% /
在 Filesystem 那一列下看到的就是你要找的设备名。我这个是在 VPS 上跑的命令,你的输出可能会不同。
确认磁盘名称无误后,再执行一次创建存储池的命令即可:
zpool create pool /dev/sda
👉 确认无误后,可以直接跳到 创建用于 LXD 的 ZFS 数据集 部分。
☁️ 在 VPS 上通过虚拟磁盘创建 ZFS 存储池
如果你像我一样在 VPS 上操作,通常是无法直接访问整块物理磁盘的,因此我们需要先创建一个虚拟磁盘文件。
首先查看当前可用的磁盘空间:
df -h /
示例输出:
Filesystem Size Used Avail Use% Mounted on
/dev/vda2 60G 11G 47G 18% /
然后创建一个目录来存放虚拟磁盘文件:
mkdir -p /var/zfs
这条命令会创建 /var/zfs 目录,带上的 -p 参数确保即使上层目录不存在也会一并创建。
可以用下面的命令验证是否成功:
ls /var/zfs
如果目录不存在,会看到类似这样的信息:
ls: cannot access '/var/non-existing': No such file or directory
我这里有 47G 可用空间,所以决定分配 24G 给虚拟磁盘。你可以根据自己实际的磁盘空间选择合适的大小:
fallocate -l 24G /var/zfs/zfs.img
这会在 /var/zfs/zfs.img 创建一个 24GB 的虚拟磁盘文件。文件名可以自定义。
接下来将这个镜像文件映射到一个可用的 loop 设备:
losetup -fP /var/zfs/zfs.img
这条命令会将该镜像文件挂载到一个空闲的 loop 设备上。
用以下命令获取被映射的 loop 设备名称:
LOOPDEV=$(losetup -a | grep zfs.img | cut -d: -f1)
可以通过下面的命令验证:
echo ${LOOPDEV}
例如,你可能会看到输出为 /dev/loop3。
最后,用这个 loop 设备创建 ZFS 存储池:
zpool create pool $LOOPDEV
这样就使用虚拟磁盘创建了名为 pool 的 ZFS 存储池。
为 LXD 创建 ZFS 数据集
当你的 ZFS 存储池准备好之后,下一步就是在里面创建一个数据集:
zfs create pool/lxd
这条命令会在现有的 pool 存储池中创建一个名为 lxd 的数据集。
如果你希望启用重复数据删除功能(deduplication),可以设置 dedup 属性为 on:
zfs set dedup=on pool/lxd
要查看数据集是否创建成功以及其各项属性,可以执行:
zfs list
示例输出:
NAME USED AVAIL REFER MOUNTPOINT
pool 142K 22.8G 24K /pool
pool/lxd 24K 22.8G 24K /pool/lxd
从输出中可以看到,pool/lxd 数据集已成功创建。
虽然看起来步骤较多,但我尽可能加入了每一步的验证方式和可能遇到的问题,确保过程顺利。
使用 lxd init 配置 LXD
现在终于可以开始初始化 LXD 了,运行以下命令:
lxd init
这会启动一个交互式的配置流程,你会被依次询问多个问题:
Would you like to use LXD clustering? (yes/no) [default=no]:
如果你没有多台服务器或设备,不需要集群功能,直接回车或输入 no。
Do you want to configure a new storage pool? (yes/no) [default=yes]:
虽然我们之前已经配置了存储,但这一步是为了把它关联到 LXD 中。
Name of the new storage pool [default=default]:
这个名字不需要和你的 ZFS 池名一致,自定义即可。
Name of the storage backend to use (powerflex, zfs, btrfs, ceph, dir, lvm) [default=zfs]:
确保选择 zfs,如果系统未安装 ZFS,这个选项可能不会出现。
Create a new ZFS pool? (yes/no) [default=yes]: no
不需要,谢谢。我们已经手动创建了 ZFS 存储池。
Name of the existing ZFS pool or dataset: pool/lxd
输入之前创建的数据集名 pool/lxd。
Would you like to connect to a MAAS server? (yes/no) [default=no]:
大多数情况选 no,除非你清楚在使用 MAAS。
Would you like to create a new local network bridge? (yes/no) [default=yes]:
- 选
yes—— 适用于大多数 VPS 情况。 - 如果在实验室环境,建议先咨询 IT 或指导老师是否允许自建网络桥接。
What should the new bridge be called? [default=lxdbr0]:
默认的 lxdbr0 没问题,除非你有特殊命名需求。
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
Would you like the LXD server to be available over the network? (yes/no) [default=no]:
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]:
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:
最后这些选项直接回车即可,默认值对大部分用户都很合适。
启动你的第一个 LXD 容器
现在可以启动你的第一个 LXD 容器了:
lxc launch ubuntu:24.04 temp
这条命令会基于 ubuntu:24.04 镜像启动一个名为 temp 的容器。
启动完成后,可以用下面的命令查看容器列表:
lxc list
示例输出:
+------+---------+------+----------------------------------------------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+---------+------+----------------------------------------------+-----------+-----------+
| temp | RUNNING | | fd42:c80b:480:4c53:216:3eff:fecd:7a41 (eth0) | CONTAINER | 0 |
+------+---------+------+----------------------------------------------+-----------+-----------+
从输出中可以确认容器 temp 正在运行中。
⚠️ 没有 IPv4 地址?别急,这样修
你可能会发现容器没有 IPv4 地址,这意味着你无法通过 SSH 访问它,也不能对外暴露服务。
👉 如果你的容器 已有 IPv4 地址 并打算发布镜像,可跳转到 发布并复用 LXD 容器镜像。
如果确实没有 IPv4 地址,并且你是在 VPS 上操作,那很可能是防火墙阻止了网络请求。很多 VPS 提供商默认封锁了所有端口。
为了解决这个问题,你需要正确配置防火墙,使容器可以获取 IP 并访问外部网络。可以参考官方文档:LXD 网络桥接与防火墙设置。
如果你使用的是 UFW(默认 Ubuntu 的防火墙工具),可以执行以下命令(来自官方文档):
# 允许容器从宿主机获取 IP(DHCP)
sudo ufw allow in on lxdbr0 to any port 67 proto udp
sudo ufw allow in on lxdbr0 to any port 547 proto udp
# 允许容器解析域名(DNS)
sudo ufw allow in on lxdbr0 to any port 53
# 允许容器访问外网
CIDR4="$(lxc network get lxdbr0 ipv4.address | sed 's|\.[0-9]\+/|.0/|')"
CIDR6="$(lxc network get lxdbr0 ipv6.address | sed 's|:[0-9]\+/|:/|')"
sudo ufw route allow in on lxdbr0 from "${CIDR4}"
sudo ufw route allow in on lxdbr0 from "${CIDR6}"
配置完后,重启容器:
lxc restart temp
然后查看容器状态:
lxc list
你应该能看到 IPv4 地址了,例如:
+------+---------+-----------------------+----------------------------------------------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+---------+-----------------------+----------------------------------------------+-----------+-----------+
| temp | RUNNING | 10.249.126.223 (eth0) | fd42:c80b:480:4c53:216:3eff:fecd:7a41 (eth0) | CONTAINER | 0 |
+------+---------+-----------------------+----------------------------------------------+-----------+-----------+
进入容器:
lxc exec temp bash
提示符可能变成这样:
root@temp:~#
注意提示符现在显示容器主机名,说明你已经进入容器环境。像操作普通 Linux 一样使用即可。退出容器输入:
exit
如何发布并复用 LXD 容器镜像
如果你的容器已经正常分配了 IPv4 地址,就可以像平常一样在终端中安装软件包。一旦你对容器的配置满意了,可以将它发布为一个本地镜像,方便今后重复使用。
首先,停止容器:
lxc stop temp
然后使用 publish 命令将容器发布为镜像:
lxc publish temp --alias template --public
可以用下面的命令查看本地镜像列表:
lxc image list
你设置的别名(例如 template)应该会出现在输出中,例如:
+----------+--------------+--------+---------------------------------------------+--------------+-----------+-----------+------------------------------+
| ALIAS | FINGERPRINT | PUBLIC | DESCRIPTION | ARCHITECTURE | TYPE | SIZE | UPLOAD DATE |
+----------+--------------+--------+---------------------------------------------+--------------+-----------+-----------+------------------------------+
| template | 5c72fbce13bc | yes | Ubuntu 24.04 LTS server (20250610) | x86_64 | CONTAINER | 430.77MiB | Jun 18, 2025 at 8:21pm (UTC) |
+----------+--------------+--------+---------------------------------------------+--------------+-----------+-----------+------------------------------+
| | 9c73fb6ca4c2 | no | ubuntu 24.04 LTS amd64 (release) (20250610) | x86_64 | CONTAINER | 258.29MiB | Jun 18, 2025 at 7:52pm (UTC) |
+----------+--------------+--------+---------------------------------------------+--------------+-----------+-----------+------------------------------+
上面那个没有别名的镜像,是你最初用来创建 temp 容器的基础镜像 —— 大概率是通过如下命令创建的:
lxc launch ubuntu:24.04 temp
🔍 ZFS 增量存储在 LXD 中是怎么工作的
本节主要是探索 ZFS 如何存储和组织你的容器数据。 如果你对 ZFS 的细节不太感兴趣,而是想要通过 SSH 连接到容器,你可以直接跳转到这里。
否则,你已经看到本指南的最后一节了 —— 感谢一路阅读!
你可以通过以下命令查看 ZFS 的存储情况:
zfs list
示例输出:
NAME USED AVAIL REFER MOUNTPOINT
pool 691M 22.1G 24K /pool
pool/lxd 678M 22.1G 24K legacy
pool/lxd/buckets 24K 22.1G 24K legacy
pool/lxd/containers 190M 22.1G 24K legacy
pool/lxd/containers/temp 190M 22.1G 667M legacy
pool/lxd/custom 24K 22.1G 24K legacy
pool/lxd/deleted 144K 22.1G 24K legacy
pool/lxd/deleted/buckets 24K 22.1G 24K legacy
pool/lxd/deleted/containers 24K 22.1G 24K legacy
pool/lxd/deleted/custom 24K 22.1G 24K legacy
pool/lxd/deleted/images 24K 22.1G 24K legacy
pool/lxd/deleted/virtual-machines 24K 22.1G 24K legacy
pool/lxd/images 487M 22.1G 24K legacy
pool/lxd/images/9c73fb6ca4c2ae7dd357696a2e16ff8ac2f140090deab77b95a24add2386a55a 487M 22.1G 487M legacy
pool/lxd/virtual-machines 24K 22.1G 24K legacy
你可以从中识别出 temp 容器和原始镜像(9c73fb6ca4c2)。
发布容器后,可以删除 temp 容器来释放空间:
lxc rm temp
此时的 ZFS 结构大致如下,唯一变化就是 temp 不见了:
NAME USED AVAIL REFER MOUNTPOINT
pool 499M 22.3G 24K /pool
pool/lxd 487M 22.3G 24K legacy
pool/lxd/buckets 24K 22.3G 24K legacy
pool/lxd/containers 24K 22.3G 24K legacy
pool/lxd/custom 24K 22.3G 24K legacy
...
pool/lxd/images 487M 22.3G 24K legacy
pool/lxd/images/9c73fb6ca4c2ae7dd357696a2e16ff8ac2f140090deab77b95a24add2386a55a 487M 22.3G 487M legacy
pool/lxd/virtual-machines 24K 22.3G 24K legacy
说实话,这有点神奇 —— 你很难立即看出发布的镜像到底被存在哪。
如果你用刚发布的 template 镜像启动一个名为 cpp 的新容器:
lxc launch template cpp
你最终会看到你发布的镜像(5c72fbce13bc,别名为 template)开始占用独立的存储空间,大约为 667M。这很合理:压缩后的 template 镜像是 430.77MiB,原始镜像是 258.29MiB,两者之间的差值是约 172.48MiB。
巧的是,这个差值也接近你当初的 temp 容器所占空间(190M),考虑到某些元数据开销,172.48MiB 是完全说得通的。
而新创建的 cpp 容器只用了大约 3.19M:
NAME USED AVAIL REFER MOUNTPOINT
pool 1.15G 22.1G 24K /pool
pool/lxd 1.13G 22.1G 24K legacy
pool/lxd/buckets 24K 22.1G 24K legacy
pool/lxd/containers 3.21M 22.1G 24K legacy
pool/lxd/containers/cpp 3.19M 22.1G 668M legacy
pool/lxd/custom 24K 22.1G 24K legacy
...
pool/lxd/images 1.13G 22.1G 24K legacy
pool/lxd/images/5c72fbce13bcbdfa41285d8b3af408a38f824c38c00b6694c10a4cdf814dae46 667M 22.1G 667M legacy
pool/lxd/images/9c73fb6ca4c2ae7dd357696a2e16ff8ac2f140090deab77b95a24add2386a55a 487M 22.1G 487M legacy
pool/lxd/virtual-machines 24K 22.1G 24K legacy
接着你又用 template 镜像启动了另一个容器 another-cpp:
lxc launch template another-cpp
ZFS 的使用情况变成这样:
NAME USED AVAIL REFER MOUNTPOINT
pool 1.15G 22.1G 24K /pool
pool/lxd 1.13G 22.1G 24K legacy
pool/lxd/buckets 24K 22.1G 24K legacy
pool/lxd/containers 5.11M 22.1G 24K legacy
pool/lxd/containers/another-cpp 1.88M 22.1G 667M legacy
pool/lxd/containers/cpp 3.21M 22.1G 668M legacy
pool/lxd/custom 24K 22.1G 24K legacy
...
pool/lxd/images 1.13G 22.1G 24K legacy
pool/lxd/images/5c72fbce13bcbdfa41285d8b3af408a38f824c38c00b6694c10a4cdf814dae46 667M 22.1G 667M legacy
pool/lxd/images/9c73fb6ca4c2ae7dd357696a2e16ff8ac2f140090deab77b95a24add2386a55a 487M 22.1G 487M legacy
pool/lxd/virtual-machines 24K 22.1G 24K legacy
此时,两个容器 cpp 和 another-cpp 都在运行,并且额外占用的空间非常少。
出于好奇,你在 another-cpp 里安装了 neovim。我们来看空间变化:
NAME USED AVAIL REFER MOUNTPOINT
pool 1.30G 22.0G 24K /pool
pool/lxd 1.28G 22.0G 24K legacy
pool/lxd/buckets 24K 22.0G 24K legacy
pool/lxd/containers 160M 22.0G 24K legacy
pool/lxd/containers/another-cpp 157M 22.0G 791M legacy
pool/lxd/containers/cpp 3.21M 22.0G 668M legacy
...
pool/lxd/images 1.13G 22.0G 24K legacy
pool/lxd/images/5c72fbce13bcbdfa41285d8b3af408a38f824c38c00b6694c10a4cdf814dae46 667M 22.0G 667M legacy
pool/lxd/images/9c73fb6ca4c2ae7dd357696a2e16ff8ac2f140090deab77b95a24add2386a55a 487M 22.0G 487M legacy
pool/lxd/virtual-machines 24K 22.0G 24K legacy
现在 another-cpp 使用了大约 157MB,正好体现了安装 neovim 和其他依赖所占用的空间。
💡 ZFS 增量存储到底省了多少
在这种设置下,ZFS 的增量存储特性真正发挥了优势:
- 基于同一个镜像启动的容器能够高效共享存储空间。
- 只有变更的部分会被写入,从而保持磁盘使用的最小化。
- 即使某个容器产生了较大的差异,相较于完整复制,其空间节省仍然非常可观。
因此,无论你的容器保持原样,还是经过了定制,ZFS 都能为你带来存储上的优势。
通过 SSH 连接 LXD 容器:端口转发与防火墙配置(可选)
这一节是可选的,但如果你想从远程机器通过 SSH 连接到容器,它会很有用。
要将容器的 SSH 端口暴露到外部网络,可以使用以下命令:
lxc config device add cpp sshproxy proxy listen=tcp:0.0.0.0:3000 connect=tcp:127.0.0.1:22
没错,它看起来有点长 —— 但你只需要改下面这两处:
- 把
cpp替换成你的容器名。 - 把
3000换成你想对外开放的端口号。
如果成功了,你会看到:
Device sshproxy added to cpp
🔥 别忘了防火墙 —— 用 UFW 开放端口
如果你是第一次接触这些,一定别忘了在防火墙里打开端口,这样外部才能访问。
比如,使用 ufw 的话:
ufw allow 3000
这个命令会允许 3000 端口的流量,输出看起来像这样:
Rule added
Rule added (v6)
你可以通过以下命令确认规则是否添加成功:
ufw status
Sample output:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
67/udp on lxdbr0 ALLOW Anywhere
547/udp on lxdbr0 ALLOW Anywhere
53 on lxdbr0 ALLOW Anywhere
3000 ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
67/udp (v6) on lxdbr0 ALLOW Anywhere (v6)
547/udp (v6) on lxdbr0 ALLOW Anywhere (v6)
53 (v6) on lxdbr0 ALLOW Anywhere (v6)
3000 (v6) ALLOW Anywhere (v6)
Anywhere ALLOW FWD 10.219.247.0/24 on lxdbr0
Anywhere (v6) ALLOW FWD fd42:2bf9:abb2:6256::/64 on lxdbr0
🧑💻 终于,SSH 进你的 LXD 容器
注意你需要使用服务器的 IP 地址。用户名是出现在终端提示符 @ 前面的那个 —— 例如在 root@cpp:~# 中,用户名是 root,不是 cpp。端口则是你之前指定的那个。
下面是一个示例命令:
ssh root@155.xxx.xxx.xxx -p 3000
如果一切正常,它会提示你输入密码:
root@155.xxx.xxx.xxx's password:
如果你还没给这个用户设置密码,可以回到容器中运行:
passwd
示例:
root@cpp:~# passwd
New password:
Retype new password:
passwd: password updated successfully
💡 提示:输入密码时不会显示任何内容 —— 这是 Linux 在输入密码时的正常行为。
🚫 出现 “Permission denied (publickey)”?别慌
如果你看到这个错误:
root@155.xxx.xxx.xxx: Permission denied (publickey).
这通常说明你的 SSH 服务器被配置为不允许密码登录。要解决这个问题:
vim /etc/ssh/sshd_config
在容器内部编辑 SSH 配置文件:
vim /etc/ssh/sshd_config
找到并修改以下行:
PasswordAuthentication yes
如果你是以 root 身份登录,还要确认这一行:
PermitRootLogin yes
保存文件后,重启 SSH 服务:
systemctl restart ssh
现在你应该可以使用密码通过 SSH 连接了。
🛠️ 还是被拒?修复 sshd_config 的覆盖问题
如果你还是无法访问,别担心 —— 我自己也遇到过这个问题。
在你的 /etc/ssh/sshd_config 文件中,可能会有这样一行:
Include /etc/ssh/sshd_config.d/*.conf
这表示 /etc/ssh/sshd_config.d/ 目录下的所有 .conf 文件都有可能覆盖主配置文件中的设置。
要检查这些文件,运行:
ls -l /etc/ssh/sshd_config.d/
示例输出:
total 1
-rw-r--r-- 1 root root 26 Jun 10 12:54 60-cloudimg-settings.conf
在这个例子中,只有一个覆盖文件。查看里面的内容:
cat /etc/ssh/sshd_config.d/*.conf
如果你看到类似这样的内容:
PasswordAuthentication no
那问题就找到了。它会覆盖你在主配置中设定的内容,并禁用了密码登录。
打开刚才看到的那个文件:
vim /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
你有两个选择:要么用 # 注释掉那一行(比如 # PasswordAuthentication no),要么显式设置为允许密码登录:
PasswordAuthentication yes
然后重启 SSH 服务:
systemctl restart ssh
确认更改是否生效:
sshd -T | grep passwordauthentication
你应该会看到:
passwordauthentication yes
✅ 再来一次 —— 这次该成了
现在,再次尝试登录:
ssh root@155.xxx.xxx.xxx -p 3000
如果成功了 —— 欢迎进入!
🎉 收尾:Hello World!
庆祝一下吧,我们来编译个经典示例:
root@cpp:~# vim main.cpp
[New] 6L, 71B written
root@cpp:~# g++ main.cpp
root@cpp:~# ./a.out
Hello World!
尽情享受你的开发环境吧!