在上一篇文章中,我引入了 Nix 管理 Arch 上的软件包。不久之后,抱着好奇与探索的心态,我参加了国内爱好者举办的 NixOS Meetup #2,社区的分享让我获益匪浅。

Nix,你好!
最近在配置 PVE 上的多个 LXC 实例时,为了解决环境配置的重复劳动问题,让我又一次想到了 Nix。Nix 打包后的环境天然具有只读属性,配合宿主机的 ZFS 共享存储,理论上可以实现所有 LXC 实例使用同一套 Nix 环境。
我去研究了一下 Home Manager using Nix 的实现,它其实是通过 activate 脚本进行的环境初始化。这样的话,我们只需要在其他 LXC 中调用这个 activate 脚本就可以实现配置复用了。本文简单记录了研究和配置的过程。
$ ls -lah /nix/store/6mq4hx9nhp5w114bv9j15q3fnbgr60vx-home-manager-generation
Permissions Size User Date Modified Name
.r-xr-xr-x 13k root 1 Jan 1970 activate
dr-xr-xr-x - root 1 Jan 1970 bin
.r--r--r-- 0 root 1 Jan 1970 extra-dependencies
.r--r--r-- 2 root 1 Jan 1970 gen-version
.r--r--r-- 6 root 1 Jan 1970 hm-version
lrwxrwxrwx - root 1 Jan 1970 home-files -> /nix/store/77016ziha4gndr065v97m0si62gi8a0x-home-manager-files
lrwxrwxrwx - root 1 Jan 1970 home-path -> /nix/store/bn5xzb5nrav7w1fp2mc31gx64paws9sq-home-manager-path
打包后的 home-manager 环境
我的总体设计思路为:由一个 LXC 专门负责 Nix 环境的部署,其他 LXC 以只读的方式使用该 /nix 目录。那么第一步就是创建一个 nix 文件系统,在 PVE shell 中执行:
# 使用 zstd 压缩
# 关闭最后被读取/访问的时间,避免更新元数据和写入损耗
zfs create ssd-pool/nix_store \
-o compression=zstd \
-o atime=off这里使用了 zstd 压缩,并关闭了最后被读取/访问的时间,避免更新元数据和写入损耗。我们可以通过 zfs list 查看当前的所有 ZFS 文件系统。
接下来创建名称为 Nix 的特权容器,这里我选择了 Arch Linux 模板。
Important
在配置中注意不要选择无特权的容器,在配置后,去选项 → 功能中开启嵌套,否则 systemd 无法正常加载,会获取不到 IP。
默认的 Arch 模板需要更新,除此之外,新版本的 pacman 中引入了 alpm 功能,使用 Landlock 机制安全下载文件,但是在 LXC 中无法使用。这里我们一并禁用。
pacman-key --init
pacman-key --populate archlinux
pacman -Sy archlinux-keyring
pacman -Syyu git接下来在 PVE 中将 nix_store 挂载到 LXC 上:
pct set 107 -mp0 /ssd-pool/nix_store,mp=/nix安装 Nix:
curl -fsSL https://install.determinate.systems/nix | sh -s -- install我在 ~/.config/home-manager 下已经有一套 home-manager 配置了,这里给出一套可用的 flake.nix:
{
description = "Home Manager configuration for multi devices";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
home-manager,
...
}@inputs:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
# 允许非自由软件
config.allowUnfree = true;
overlays = [];
};
in
{
homeConfigurations = {
"root@Nix" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
extraSpecialArgs = { inherit inputs; };
modules = [ ./hosts/lxc-base/home.nix ];
};
};
};
}
lxc-base 对应着 LXC 通用的配置。如果希望配置多台机器,将他们的配置分为不同的文件是个很不错的选择。./hosts/lxc-base/home.nix 的内容为:
{
pkgs,
...
}:
{
imports = [
../../modules/common.nix
];
home.username = "root";
home.homeDirectory = "/root";
home.packages = with pkgs; [
nmap
socat
];
programs.zsh = {
initContent = ''
export PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH"
'';
};
home.stateVersion = "25.11";
}
接下来就可以为本地进行配置了。第一次运行时没有 home-manager,因此需要执行:
nix run home-manager/master \
-- switch \
--flake .#root@Nix -b backup-$(date +%Y%m%d%H%M%S)如果机器名 root@Nix 和配置一致,可以省略上面的 #root@Nix。之后的更新只需要执行:
home-manager switch \
--flake . \
-b backup-$(date +%Y%m%d%H%M%S)我们可以先 build 出 result:
home-manager build --flake .#root@Nix接下来就可以将 home manager 生成的 activate 配置到另一台 LXC 上了。先记录一下它的路径:
$ readlink -f ./result
/nix/store/y1azld4dq1j5rzl7aj8k36i6q8aabl73-home-manager-generation我使用的另一台 LXC 已经有了两个挂载点(mp0 和 mp1),因此这里我们以只读的方式挂载到 mp2:
pct set 110 -mp2 /ssd-pool/nix_store,mp=/nix,ro=1
直接执行上文记录的 activate 的话会报错:
$ /nix/store/xxxx-home-manager-generation/activate
readlink: missing operand
Try '/nix/store/yyy-coreutils-9.8/bin/readlink --help' for more information.
dirname: missing operand
Try '/nix/store/yyy-coreutils-9.8/bin/dirname --help' for more information.
Starting Home Manager activation
/nix/store/zzz-home-manager-generation/activate: line 192: nix-build: command not found主要是因为缺失了 nix-build,可以通过和上面类似的方法解决。首先获取 nix-build 的软链接:
$ dirname $(readlink -f $(which nix-store))
/nix/store/a6y4cspbx1b5x3s2gw6fq4aabmsdsa4g-determinate-nix-3.15.1/bin之后在新的 LXC 中更新 PATH:
export PATH=$PATH:/nix/store/a6y4cspbx1b5x3s2gw6fq4aabmsdsa4g-determinate-nix-3.15.1/bin再执行一次 activate 即可:
$ /nix/store/y1azld4dq1j5rzl7aj8k36i6q8aabl73-home-manager-generation/activate
Starting Home Manager activation
Activating checkFilesChanged
Activating checkLinkTargets
Activating writeBoundary
Creating new profile generation
Activating installPackages
installing 'home-manager-path'
Activating linkGeneration
Creating home file links in /root
Activating onFilesChange
Activating reloadSystemd为了方便起见,我们可以使用脚本进行更新。我让 Gemini 为我生成了一份:
#!/bin/bash
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <User@ConfigName> <TargetIP>"
echo "Example: $0 root@Nix 192.168.31.255"
exit 1
fi
FULL_CONFIG_NAME="$1" # 例如: root@Nix
TARGET_IP="$2" # 例如: 192.168.31.255
REMOTE_USER="${FULL_CONFIG_NAME%%@*}"
# 获取 Builder 中的 Nix 路径
NIX_BIN_PATH=$(dirname $(readlink -f $(which nix-store)))
echo "🔨 Building configuration: $FULL_CONFIG_NAME..."
CONFIG_PATH=$(nix build .#homeConfigurations."$FULL_CONFIG_NAME".activationPackage --print-out-paths)
echo "🚀 Deploying to $REMOTE_USER@$TARGET_IP..."
ssh "$REMOTE_USER@$TARGET_IP" "
export PATH=\$PATH:$NIX_BIN_PATH
$CONFIG_PATH/activate
"
echo "✅ Done!"
使用方式为 bash deplay.sh <Username>@<Nix-profile> <IP>
它会构建 <Username>@<Nix-profile> 的配置并部署到指定 IP 的 <Username> 用户下。通过这种方式,我们就可以一键式在不同的 LXC 中使用同一套 Nix 环境,不但能避免重复部署的问题,还能以文件的形式定义配置,最大化空间的利用。
总的来说,Nix 确实给我带来了越来越多的惊喜。(当然我还是没有切换到 NixOS 的计划)。在 AI 时代,基础设施建设变得愈发重要,Meetup 上也有众多关于打包方式的讨论,我认为 Nix 作为声明式的文件配置,有着更独特的应用空间和应用价值,而且它与常见的打包方式并不冲突,有着巨大的潜力。