↓ 跳过正文

NixOS(十一):服务、容器与网络

·4243 字·9 分钟
锦李本鲤
作者
锦李本鲤
光锥之内,皆为命运。
NixOS系列 - 这篇文章属于一个选集。
§ 11: 本文

缘起
#

在上一篇中,我们介绍了系统状态与数据备份。开始部署服务之后,这些内容就会连在一起:软件从哪里来,程序怎样启动,数据写在哪里,以及哪些电脑能够访问它。

之前我在Docker使用方法中介绍过用Compose整理容器。NixOS还可以直接用系统模块管理服务,所以这里整理一下两种方式的用途,以及启动关系、数据目录和网络设置。

示例使用虚构主机laptop、保留的示例域名demo.invalid和演示网页,不包含现有服务配置。下面先在本机回环地址测试,不需要真实域名或公开端口。

服务部署方式
#

常见方式可以分为三类:

方式配置位置适合的情况
NixOS服务模块services.*等系统选项已有合适模块,希望统一管理账户、配置和启动方式
自定义systemd服务systemd.services.*自己的程序或脚本,需要明确运行身份和启动条件
Docker或Podman容器Compose或相应NixOS容器选项应用已有镜像,或需要保留已有容器部署方式

安装一个软件包,不一定会启用它的服务。模块往往还会生成配置、账户和systemd单元;这些设置也不一定与上游软件的默认值完全一致。使用之前,应检查当前锁定版本提供的选项。

容器同样需要有人管理启动、更新、机密、数据和网络。可以选择NixOS的OCI容器模块,也可以继续维护Compose文件,但应明确哪个工具负责启动哪组容器,避免两套配置争用同一个端口或数据目录。

目前使用的服务组织方式
#

现在这些电脑上的容器服务,仍由Compose定义和启动。NixOS负责容器引擎、启动时准备运行配置的任务,以及一些主机服务。服务的通用Compose定义与主机差异分开保存,应用源码也可能是独立Git子模块;迁移时调整目标主机的安排,不必把整套Compose配置改写成Nix表达式。

部署时先检查加密输入和普通参数,生成权限受限的运行时文件,取得对应的Compose组合,再重建或重新创建受影响的服务。NixOS重建和Compose部署不是同一个操作:只修改容器镜像或主机参数时,不一定需要重建整个系统,但仍要重新生成所需运行配置并验证实际服务。

访问入口主要由Traefik与相应隧道或直接路由提供。需要代理的容器加入共享代理网络,能通过这个网络转发的应用不另外发布宿主机端口。路由与主机差异分开记录,公网入口和直接入口分别验证;端口没有发布,也不表示代理提供的入口没有暴露。

实际迁移时,还要一并处理持久数据、解密权限、镜像可用性和原位置的停止状态。服务能够启动只是其中一步,健康与访问路径都要在目标位置检查。

下面用Nginx和一个小型容器说明服务模块、监听地址与代理关系。它们是教学例子,不是当前主机的边缘代理配置;DynamicUser示例也只是说明systemd如何管理目录,并不表示现有服务全部采用动态账户。

使用NixOS服务模块
#

配置Nginx演示网页
#

在已有配置中创建modules/service-demo.nix,并将它导入主机的模块列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{ pkgs, ... }:
let
  demoSite = pkgs.writeTextDir "index.html" ''
    <!doctype html>
    <title>Service demo</title>
    <p>NixOS service demo</p>
  '';
in
{
  services.nginx = {
    enable = true;
    virtualHosts."demo.invalid" = {
      listen = [ { addr = "127.0.0.1"; port = 8088; } ];
      root = demoSite;
    };
  };
}

这份配置启用Nginx,并让演示虚拟主机监听127.0.0.1:8088。writeTextDir生成store中的静态网页,适合这里没有机密、无需运行时修改的内容。真实上传文件和数据库应使用另外的数据目录。

这是假设端口空闲、可以调整Nginx的演示环境。已有Nginx配置时,应检查现有虚拟主机和监听设置,保留它们并避免名称冲突。一个演示主机监听回环地址,不会自动把其他虚拟主机也变成仅本机可访问。

相关选项可以参考NixOS Nginx模块。

构建与检查
#

1
2
3
git add modules/service-demo.nix
nix flake check --no-build --show-trace
nixos-rebuild build --flake .#laptop

这里的laptop需要替换为自己的输出名称。构建后,按第五篇的流程检查并应用配置,再查看服务和网页:

1
2
3
4
5
systemctl status nginx.service --no-pager
sudo journalctl -u nginx.service -b --no-pager
ss -ltn 'sport = :8088'
curl --fail --silent --show-error \
  -H 'Host: demo.invalid' http://127.0.0.1:8088/

正常情况下,可以看到回环地址上的监听,以及包含NixOS service demo的HTML。Host请求头选择示例虚拟主机,不需要将demo.invalid配置到DNS里。

服务显示active,说明systemd认为它处于相应运行状态;端口监听和网页内容则验证另外两件事。真实服务还需要测试登录、数据读写等实际功能,不能仅凭启动状态判断可用。

systemd启动关系
#

一个服务往往依赖挂载、数据库或运行时机密。systemd把“是否拉起另一个单元”和“启动顺序”分别处理:

设置作用
wantedBy将服务加入相应目标的启动安排
wants拉起所列单元,其失败通常不会直接阻止本服务
requires建立更强的依赖关系,失败处理还与启动顺序等设置有关
after或before安排同时启动的单元之间的顺序,本身不会拉起另一个单元

比如,数据库客户端可能既需要数据库服务被启动,也需要它先启动。但数据库进程启动,不一定已经能接受连接。程序仍需要重试、超时或相应的就绪检查。

network-online.target表示系统所配置的网络等待机制完成,并不保证某个远程网站或数据库一直可达。不要给所有服务都机械地添加它;本机静态网页通常没有这种需求。

相关概念可以参考systemd单元说明。容器的Compose也有类似区别:普通depends_on主要安排启动关系,使用service_healthy时才会等待指定的健康检查;健康检查通过之后,依赖服务仍可能再次失败。可以参考Compose依赖说明。

服务身份与数据目录
#

自己的脚本可以通过systemd声明运行身份和目录。例如,下面是一个独立的演示模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  systemd.services.demo-state = {
    description = "Write a demonstration state file";
    wantedBy = [ "multi-user.target" ];
    serviceConfig = {
      Type = "oneshot";
      DynamicUser = true;
      StateDirectory = "demo-state";
      StateDirectoryMode = "0700";
    };
    script = ''
      printf 'demonstration state\n' > "$STATE_DIRECTORY/status.txt"
    '';
  };
}

StateDirectory让systemd准备持久状态目录,并通过STATE_DIRECTORY提供给程序。DynamicUser使用临时分配的运行身份,和systemd管理的目录配合使用;不要拿每次可能变化的UID去手工安排其他目录的所有权。

可以这样运行和检查:

1
2
3
sudo systemctl start demo-state.service
systemctl show demo-state.service -p Result -p ExecMainStatus
sudo cat /var/lib/demo-state/status.txt

它是一次性任务,成功退出后显示inactive是正常情况。这里写入的只是演示文本,所以可以读取;真实服务的状态文件可能含有私人内容,不能照搬输出到日志或聊天中。

在使用DynamicUser时,持久目录可能通过/var/lib/private等方式保护,备份时应确认实际路径和权限。RuntimeDirectory则用于临时运行文件,生命周期与服务相关,不能当作持久存储。相关说明见systemd目录与运行身份文档。

系统回滚不会自动恢复这些状态文件。容器的数据卷也一样,软件版本和数据恢复需要分别处理。机密的运行时提供方法,可以参考第九篇。

使用容器部署服务
#

如果选择容器方式,可以在一个新的演示目录里创建compose.yaml:

1
2
3
4
5
6
7
services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "127.0.0.1:8088:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro

这里使用与前面相同的宿主机端口,因此应先移除或调整原生Nginx演示的8088监听,不能把两种部署直接同时启动。需要已有Docker与Compose环境;这份文件不会替我们配置容器引擎。

镜像标签用于说明配置格式,不代表长期维护时应一直停留在这个版本。真实部署应选择仍受支持的版本,记录实际镜像摘要,并安排更新。

在演示目录执行:

1
2
3
4
5
6
mkdir -p site
printf '<p>Container service demo</p>\n' > site/index.html
docker compose -p service-demo config
docker compose -p service-demo up -d
docker compose -p service-demo ps
curl --fail --silent --show-error http://127.0.0.1:8088/

网页应包含Container service demo。site是宿主机目录,只读挂载到容器里;如果应用需要写入数据库或上传文件,应另外明确写入位置和备份方法。

这里指定127.0.0.1,只在宿主机回环地址发布端口。省略地址的端口映射通常发布到所有宿主机地址,不能把它当作默认的私有访问方式。Docker版本和网络模式也会影响可达性;例如官方文档记录了旧版本回环发布的同网段访问问题。可以参考Docker端口发布说明。

结束演示时,可以在这个目录停掉这组容器:

1
docker compose -p service-demo down

宿主机的site目录仍然保留。真实服务不要随意添加-v删除数据卷,也不要把停止容器当作已经完成数据备份。

网络访问与反向代理
#

网络访问需要分别考虑监听地址、主机防火墙、路由和应用权限。开放防火墙端口不会让一个仅监听127.0.0.1的程序直接接受局域网连接;反过来,监听所有地址也不表示所有网络都能通过。

Docker会管理端口转发和防火墙规则,所以容器发布端口的流量,不一定经过与普通主机服务相同的规则。不能仅看NixOS的allowedTCPPorts就断言某个容器端口没有暴露。可以参考Docker防火墙说明,并从实际访问位置测试。

反向代理可以统一入口,将请求转发给后端。例如,使用宿主机Nginx代理上面的容器时,可以添加:

1
2
3
4
5
6
{
  services.nginx.virtualHosts."demo.invalid" = {
    listen = [ { addr = "127.0.0.1"; port = 8089; } ];
    locations."/".proxyPass = "http://127.0.0.1:8088";
  };
}

这个片段需要已经启用Nginx,并应替换前面同名演示虚拟主机的定义,不要与它重复合并。应用后检查:

1
2
curl --fail --silent --show-error \
  -H 'Host: demo.invalid' http://127.0.0.1:8089/

此时入口是8089,后端仍是8088。如果Nginx本身也在容器里,127.0.0.1通常指向那个容器自己的网络环境,不能直接当作宿主机或另一个容器;这时应按网络安排使用相应服务名或宿主机地址。

真实公开入口还需要域名、TLS、认证及相应的访问策略。反向代理本身不会自动给应用加上登录保护。本例只说明转发关系,没有配置公开站点。

问题解决
#

我通常建议按以下顺序检查:

  1. 配置是否导入了正确主机,构建和应用是否完成;
  2. 服务或容器是否启动,日志里的首个错误是什么;
  3. 程序监听了哪个地址和端口,是否有冲突;
  4. 从后端、本机入口、局域网或外部访问位置分别测试;
  5. 最后检查DNS、TLS、认证和实际业务功能。

例如,本机后端正常、代理返回错误,可以先检查代理目标和它所处的网络环境;代理正常、另一台电脑不能访问,再检查入口监听、防火墙与路由。修改时尽量一次只调整一个环节,避免为了让页面打开而同时关闭多处访问限制。

推荐的使用方法
#

我建议先检查是否有合适的NixOS服务模块。自己写的程序可以用systemd明确运行身份和数据目录;已有成熟容器部署的应用,也可以继续用Compose,但应统一配置位置并记录启动方式。

让AI协助时,可以要求它说明服务账户、持久数据、机密来源、监听地址和启动条件。配置修改后分别验证进程、访问范围和实际功能,并按上一篇的办法准备数据恢复。

下一篇将介绍更新与多台电脑的部署,包括输入更新、构建资源安排和远程验证。

NixOS系列 - 这篇文章属于一个选集。
§ 11: 本文