从一行配置,到一个操作系统
会一点 Linux,但看见 { config, pkgs, lib, ... }: 就犯迷糊?本文用三层心智模型 + 三张架构图,把 Flake、模块系统与构建机制一次讲清。
三层心智模型
先建立整体认识:NixOS 体系由三层叠加而成,任何细节都能归位到某一层。
三层各自回答什么问题
| 层 | 名称 | 回答的问题 | 主要概念 |
|---|---|---|---|
| 第一层 | Flake(组织层) | 项目怎么组织、依赖怎么锁 | inputs · outputs · flake.lock |
| 第二层 | 模块系统(配置层) | 一台操作系统怎么配 | imports · options · config · lib · pkgs |
| 第三层 | Nix 构建系统 | 配置怎么变成可运行系统 | Nix Store · Generation · 激活 |
三层的衔接桥梁是 nixosSystem { system, modules }——它接收 Flake 的输入,把模块列表交给模块系统求值。
Nix 语言一分钟速览
看懂后续所有示例所需的最小语法集。如果已经熟悉,可直接跳到下一章。
| 语法 | 写法 | 说明 |
|---|---|---|
| 属性集 | { a = 1; b = "x"; } | 类似对象 / 字典 |
| 访问属性 | config.my.ai.enable | 点号取值 |
| 函数 | x: x + 10 | 冒号前是参数 |
| 多参函数 | { config, pkgs, lib, ... }: { ... } | 参数是一个属性集,... 接收额外参数 |
| 绑定 | let cfg = config.my.ai; in ... | 局部命名 |
| 引入文件 | import ./network.nix | 求值表达式机制 |
| 列表 | [ pkgs.git pkgs.curl ] | 空格分隔 |
| 惰性求值 | — | 只算被需要到的东西,允许自引用 |
import ≠ 模块系统的 imports。前者是表达式求值机制,后者是收集模块的选项。
import vs imports —— 语言层与配置层的区别
它俩属于两个不同的“世界”:import 是 Nix 语言的求值机制(把文件变成“一个值”);imports 是 NixOS 模块系统的收集选项(把模块“拉进队伍”一起合并)。
① import:Nix 语言的表达式求值机制
任何 .nix 文件都可以被 import:读取文件 → 当 Nix 表达式求值 → 得到一个值(属性集、函数、数字…)。
# data.nix —— 一个纯数据文件 { name = "nginx"; port = 80; }
# 使用处 let cfg = import ./data.nix; # import 求值 → 得到属性集 { name = "nginx"; port = 80; } in cfg.name # → "nginx"
如果文件里写的是函数,import 得到的还是那个函数本身(未调用):
# module-file.nix —— 一个模块形状的函数 { config, pkgs, ... }: { services.openssh.enable = true; } # 使用处 let m = import ./module-file.nix; # m = 那个函数,还没执行
类比:像 JavaScript 的 require / Python 的 import,执行完那个文件,把它的导出值拿回来。
② imports:NixOS 模块系统的收集选项
写在模块主体的顶层,声明“这次配置求值要合并哪些模块”。不负责求值,只负责收集,合并交给模块系统。
{ ... }:
{
imports = [ ./network.nix ./security.nix ];
services.openssh.enable = true;
}
③ 关键区别
| 维度 | import | imports |
|---|---|---|
| 所属层级 | Nix 语言(通用机制) | NixOS 模块系统(专用选项) |
| 本质 | 求值表达式 | 声明性配置字段 |
| 作用 | 把文件求值成“一个值” | 收集模块参与合并 |
| 有返回值吗 | 有(属性集/函数/…) | 无,只是声明集合 |
| 出现位置 | 任意表达式里(let、属性值…) | 模块主体顶层字段 |
| 类比 | “加载并执行文件” | “组装组件清单” |
④ 它们其实经常配合出现
{ config, pkgs, lib, ... }:
let
base = import ./base.nix; # ① Nix import:把文件求值成模块函数
in
{
imports = [ base ./network.nix ]; # ② 模块 imports:把模块纳入合并
}
甚至可以写成一行:imports = [ (import ./base.nix) ]; —— 括号内是 import 求值,括号外是 imports 收集。
import 回答“这个文件求值完是什么(值)”;imports 回答“这次系统配置要合并哪些模块(集合)”。Flake —— 项目组织层
项目怎么组织、依赖怎么锁、要构建哪些主机——这些都由 Flake 在“比系统配置更高一层”回答。
传统 Linux vs NixOS vs Flake
传统 Linux 靠命令操作:
# 告诉机器“怎么做”
apt install nginx
systemctl enable nginx
systemctl start nginx
NixOS 用配置描述目标状态:
services.nginx.enable = true;
而 Flake 在更高一层,回答:用哪个版本的 Nixpkgs?构建哪些机器的配置?每台机器用哪些模块?依赖版本如何锁定?
Flake 与 NixOS Modules 的区别
| 对比 | Flake | NixOS Modules |
|---|---|---|
| 核心职责 | 项目依赖与输出管理 | 操作系统配置管理 |
| 主要文件 | flake.nix | configuration.nix、modules/*.nix |
| 核心概念 | Inputs · Outputs · Lock | Imports · Options · Config |
| 软件依赖 | 引入 Nixpkgs 等输入 | 通过 pkgs 引用软件包 |
| 多机管理 | 定义多个系统输出 | 组合各台机器的配置 |
| 是否独立使用 | 可以 | 可以 |
核心组成
一个最基本的 flake.nix:
{
description = "My NixOS System";
inputs.nixpkgs.url =
"github:NixOS/nixpkgs/nixos-25.11";
outputs = { self, nixpkgs, ... }: {
nixosConfigurations.myserver =
nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
};
}
nixos-25.11 为教学占位,实际部署请选择当前受支持的 NixOS 版本分支。函数式机制:Outputs = F(Inputs)
outputs 本质是一个接收输入、返回属性集的 Nix 函数:
输入:self + nixpkgs + 其他 Flake 依赖 ↓ outputs 函数计算 ↓ 输出:NixOS 系统配置 · 软件包 · 开发环境 · 项目检查
常见输出类型
| 输出名称 | 用途 |
|---|---|
nixosConfigurations | 定义 NixOS 系统配置 |
packages | 定义可构建的软件包 |
devShells | 定义开发环境 |
apps | 定义可通过 Nix 运行的应用 |
checks | 定义项目检查 |
formatter | 定义代码格式化工具 |
一个 Flake 可以同时提供服务器配置、桌面配置、Python 开发环境、自定义软件包与项目检查——这正是 Flake ≠ configuration.nix 的原因。
依赖锁定机制
flake.nix → 依赖解析 → flake.lock → 后续使用锁定的依赖
即使远程分支后来更新,持有原 flake.lock 的项目仍引用先前锁定的版本,减少多台机器之间的依赖漂移。
常用命令
| 命令 | 作用 |
|---|---|
nix flake metadata | 查看 Flake 信息 |
nix flake show | 查看 Flake 输出 |
nix flake check | 检查 Flake 输出是否满足要求 |
nix flake update | 更新锁定依赖 |
NixOS 模块系统 —— 配置层
一台操作系统具体怎么配?五个名字各司其职,通过求值、合并、构建变成可运行系统。
传统 vs 声明式
传统 Linux:告诉机器“怎么做”——一串操作命令,逐步修改当前系统。
NixOS:告诉系统“想要什么”——
services.nginx.enable = true;
模块系统读取配置定义,计算所需的服务、软件与文件,经 Nix 构建生成系统代际并激活。它不是 Linux 内核直接解析 Nix 配置,也不只是把一行状态翻译成三行 Shell 命令。
五个名字的职责
先看最常见的模块外形——注意第一行是函数参数列表,第二个大括号开始才是模块主体:
{ config, pkgs, lib, ... }:
{
imports = [ ./network.nix ];
services.openssh.enable = true;
}
| 名字 | 类别 | 作用 | 记忆法 |
|---|---|---|---|
imports | 模块组织 | 把其他模块纳入同一次求值 | 组装组件 |
options | 接口声明 | 声明合法选项、类型、默认值 | Schema |
config | 配置定义与读取 | 提交定义 / 读取合并结果 | 双重身份 |
pkgs | 软件依赖 | 引用 Nixpkgs 软件包产物 | 软件依赖目录 |
lib | 辅助规则 | mkIf/mkDefault/mkForce 等工具 | 框架工具库 |
真实案例:一行开关启用 Ollama
目标:主配置只写一行 my.ai.enable = true;,就启用 Ollama 服务。
项目结构:
/etc/nixos/
├── configuration.nix
└── modules/
└── ai.nix
自定义模块 modules/ai.nix:
{ config, pkgs, lib, ... }:
let
cfg = config.my.ai;
in
{
# ① 声明自定义配置接口
options.my.ai.enable =
lib.mkEnableOption "AI service";
# ② 当开关开启时,提供以下配置定义
config = lib.mkIf cfg.enable {
services.ollama.enable = true;
environment.systemPackages = [
pkgs.curl
];
};
}
主配置 configuration.nix(片段,非整份替换):
{ ... }:
{
imports = [
./modules/ai.nix
];
my.ai.enable = true;
}
| 序号 | 动作 | 参与者 |
|---|---|---|
| 1 | options 声明 my.ai.enable 为合法布尔开关 | lib |
| 2 | config.my.ai 读取开关合并结果,取别名 cfg | config |
| 3 | lib.mkIf 在开关为真时提供定义 | lib |
| 4 | services.ollama.enable = true 要求启用 Ollama | config |
| 5 | pkgs.curl 把 curl 纳入系统软件包 | pkgs |
my.ai.enable = false 后,本模块不再提供 Ollama 与 curl 的定义;但如果其他模块仍启用,它们依然可能存在。“不提供启用定义”不等于“强制所有模块禁用”。为什么 config 看起来会引用自己?
{ config, lib, ... }:
{
options.my.demo.enable = lib.mkEnableOption "demo";
config = lib.mkIf config.my.demo.enable {
services.openssh.enable = true;
};
}
表面看计算 config 需要先知道 config.my.demo.enable。但模块系统先收集选项定义,再按需计算值;lib.mkIf 把条件保留在定义层面,避免直接用 Nix if 控制模块结构时的无限递归。
合并、条件与优先级
列表类型可以合并:两个模块分别定义 allowedTCPPorts = [ 80 ] 与 [ 443 ],合并为 [ 80 443 ]。不是所有类型都这样合并,普通字符串选项同优先级不同定义通常会冲突。
默认值与覆盖:
# 基础模块 { lib, ... }: { services.openssh.enable = lib.mkDefault true; } # 机器专用模块 { ... }: { services.openssh.enable = false; }
最终值为 false:普通定义优先于 mkDefault。
协同工作流(Flake + Modules)
Flake 与模块系统通过 nixosSystem 桥接,形成“依赖 → 配置 → 构建 → 激活”的完整链路。
整体链路
flake.nix (inputs → outputs) │ ▼ nixosSystem { system, modules } ← 桥梁 │ ▼ NixOS Modules (imports / options / config / pkgs / lib) │ ▼ 模块求值与合并(类型检查、优先级、默认值) │ ▼ 系统构建与激活(Nix Store → System Generation)
nixosSystem 的作用
nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
}
它是 Nixpkgs 提供的函数,根据目标平台和模块列表建立一个 NixOS 系统配置;modules 参数指定参与求值的模块。
模块如何接收参数?
{ config, pkgs, lib, ... }:
{
services.openssh.enable = true;
environment.systemPackages = [ pkgs.git ];
}
| 参数 | 作用 |
|---|---|
config | 访问模块系统合并后的配置 |
pkgs | 访问软件包集合 |
lib | 使用 Nix 辅助函数 |
... | 接收其他模块参数 |
flake.nix 中手动逐个传递。多主机架构实战
当系统涉及多台电脑、服务器、Docker、AI 服务时,用 hosts + modules 结构把差异与复用分开。
为什么不建议把所有配置写进 flake.nix?
当系统涉及多台电脑、多台服务器、Docker 容器、AI 模型服务、网络与安全策略、开发环境时,单文件会导致:
- 文件越来越庞大;
- 配置难以复用;
- 主机之间的差异难以管理;
- 修改容易影响其他机器;
- 不利于自动化配置生成。
推荐项目结构
nixos-config/ │ ├── flake.nix ├── flake.lock ├── README.md │ ├── hosts/ │ ├── desktop/ # default.nix + hardware-configuration.nix │ ├── server/ # default.nix + hardware-configuration.nix │ └── ai-node/ # default.nix + hardware-configuration.nix │ └── modules/ ├── base.nix ├── users.nix ├── network.nix ├── security.nix ├── docker.nix ├── ollama.nix └── development.nix
| 目录或文件 | 作用 |
|---|---|
flake.nix | 定义依赖与系统输出 |
flake.lock | 锁定依赖版本 |
hosts/ | 保存每台机器的配置 |
modules/ | 保存可复用功能模块 |
hardware-configuration.nix | 保存硬件、文件系统等机器相关配置 |
编写 flake.nix(三主机)
{
description = "Multi-host NixOS";
inputs.nixpkgs.url =
"github:NixOS/nixpkgs/nixos-25.11";
outputs = { nixpkgs, ... }: {
nixosConfigurations = {
desktop = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./hosts/desktop/default.nix ];
};
server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./hosts/server/default.nix ];
};
ai-node = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./hosts/ai-node/default.nix ];
};
};
};
}
三台主机共用同一套 Nixpkgs 依赖,同时组合不同的系统模块。
编写可复用模块
# modules/development.nix { pkgs, ... }: { environment.systemPackages = [ pkgs.git pkgs.curl pkgs.htop ]; } # modules/docker.nix { ... }: { virtualisation.docker.enable = true; } # modules/ollama.nix { ... }: { services.ollama.enable = true; }
编写主机配置
# hosts/server/default.nix —— 服务器:只有 Docker { ... }: { imports = [ ./hardware-configuration.nix ../../modules/base.nix ../../modules/network.nix ../../modules/security.nix ../../modules/docker.nix ]; networking.hostName = "server"; } # hosts/ai-node/default.nix —— AI 节点:Docker + Ollama { ... }: { imports = [ ./hardware-configuration.nix ../../modules/base.nix ../../modules/network.nix ../../modules/security.nix ../../modules/docker.nix ../../modules/ollama.nix ]; networking.hostName = "ai-node"; }
这样 AI 节点拥有 Docker + Ollama,普通服务器只需要 Docker。
base.nix、network.nix、security.nix 需自行实现;这是架构示例,不是可直接替换现有系统的完整安装配置。构建与部署
| 命令 | 作用 |
|---|---|
nix flake check | 检查 Flake 输出是否满足要求 |
sudo nixos-rebuild build --flake .#ai-node | 构建但不切换当前系统 |
sudo nixos-rebuild switch --flake .#ai-node | 构建并激活新系统代际 |
switch。flake check 与 build 成功不代表网络、驱动、硬件、服务一定能正常运行,系统激活后仍需测试。常见误区汇总
两篇教程的误区去重合并成九条,每条都是一次“理解偏差 → 正确认识”的纠正。
imports 是模块系统用来收集模块的选项;Nix 的 import 是表达式求值机制。两者可以配合,但含义不同。options 只声明接口,必须有对应配置定义以及构建、激活过程。inputs 只声明依赖来源,装什么由输出定义决定。进阶方向 —— 可视化配置平台
若要开发一个通过网页配置 NixOS 的系统,可把 Flake 与模块库结合成五段流水线。
Web 可视化管理界面(选择主机 / 软件 / 网络 / AI 服务)
│
▼
结构化配置模型(JSON / Schema:参数校验、默认值、约束)
│
▼
Nix 配置生成引擎(生成主机配置、选择模块、生成 Flake 输出)
│
▼
NixOS 配置仓库(flake.nix / hosts / modules)
│
▼
验证、构建与部署(nix flake check / nixos-rebuild build / switch)
为什么复用 NixOS Modules? 模块系统已提供配置选项、类型定义、默认值、配置合并、条件定义、选项描述、软件包依赖、模块复用——可视化工具不必重新发明一套系统配置语言。
知识速查
概念、命令、心智模型三张速查表,学完随时回来翻。
概念速查表
| 你想问的问题 | 找谁 |
|---|---|
| 其他配置模块从哪里来? | imports |
| 这个配置项是否存在、是什么类型? | options |
| 这个配置项设成什么值? | 模块主体的 config / 简写定义 |
| 最终合并出来的值是什么? | 函数参数 config |
| 我要引用 Git、curl、Nginx 软件包。 | pkgs |
| 我要定义开关、条件、默认值、覆盖规则。 | lib |
| 项目用哪个 nixpkgs、构建哪些主机? | Flake 的 inputs / outputs |
| 依赖版本怎么固定? | flake.lock |
命令速查表
| 场景 | 命令 |
|---|---|
| 只求值构建,不切换 | sudo nixos-rebuild build |
| 构建并激活 | sudo nixos-rebuild switch |
| 指定 Flake 与主机 | sudo nixos-rebuild switch --flake /etc/nixos#myserver |
| 构建指定主机 | sudo nixos-rebuild build --flake .#ai-node |
| 查看 Flake 信息 | nix flake metadata |
| 查看 Flake 输出 | nix flake show |
| 检查 Flake | nix flake check |
| 更新锁定依赖 | nix flake update |
最终心智模型
再加一层:Flake 决定“用哪个 nixpkgs、构建哪些主机”,模块系统决定“每台主机内部怎么配”。