从零开始编写Guix服务:初学者教程

Lobsters Hottest 工具

摘要

本文提供了一份面向初学者的指南,教您如何在Guix中编写自定义服务,以Caddy为例,并涵盖系统配置和Scheme编程基础。

<p><a href="https://lobste.rs/s/f4arrs/writing_guix_service_from_scratch_as">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/14 00:55

# 从零开始编写 Guix 服务:面向初学者 来源:https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html 我正在将机器从 NixOS 迁移到 Guix。第一个里程碑是将我的 VPS 迁移到 Guix。我的 VPS 主要承担三个核心角色: 1. 托管我的邮件服务器 2. 托管我的 Wireguard 服务器 3. 作为代理服务器访问家庭服务器的服务(从而隐藏我的家庭 IP) 邮件服务器已正常运行,使用 Exim 作为 MTA,Dovecot 作为 IMAP 服务器。我费了一番周折才让 Exim 正常运行。原本更倾向于使用 Postfix,但官方 Guix 频道仅默认提供 Exim。当时还不知道如何定义自定义服务。 我计划在未来继续使用 Guix,因此希望深入理解它。学习创建符合自身需求的自定义服务,是掌握 Guix 的重要部分。这也引出了本文的写作动机:是时候创建自己的服务了。 对于代理服务器,我喜欢使用 Caddy。但 Guix 没有将其作为服务提供[1]。这似乎是深入探索自定义服务的绝佳时机。在查阅文档和源码的过程中,我决定记录学习过程并在此分享,作为其他新用户的指南。 本文将为 Caddy 反向代理编写自定义服务。该服务涉及通过 Guix 设置 Caddy 配置文件、创建系统用户,并配置 Shepherd 运行守护进程。这是完美的首个自定义服务案例:结构足够简单便于入门,同时涵盖了自定义服务的核心概念。此外,我在家庭服务器上运行的大多数服务本质上都是这三项操作的组合:配置用户、管理配置文件、运行守护进程。 ## 适用对象 本文适合 Guix 新手和初学者。实际上,我在写作时也在摸索前行,除阅读文档外并无太多专业知识。 ## 前置条件 唯一真正的要求是在 Guix 系统上准备好 Caddy 软件包,以便在此基础上构建服务。撰写本文时,Guix 仓库尚未包含 Caddy 软件包,但团队正在开发中。在此期间,您可参考我的[博文](https://aloysberger.com/posts/guix-packaging-a-binary-as-a-guix-beginner.html)轻松创建软件包,仅需几行代码即可完成。 您不需要精通 Scheme 语言(我本人也不擅长),但建议具备基础理解。假设您像我一样刚接触 Guix——但这不能替代官方文档。我会用个人理解重新诠释文档,建议您边阅读本文边参考官方文档。 假设您已安装 Guix 并拥有可用配置。若尚未配置,可参考[官方指南](https://guix.gnu.org/manual/devel/en/html_node/Using-the-Configuration-System.html)。最后需掌握系统重新配置方法(提示:`sudo guix system reconfigure my-config.scm`)。 引用 Guix 源码时,将采用`文件名:行号`格式。 ## 构建计划 为保持简洁,我们将定义尽可能精简的服务。计划如下: 1. 定义空服务并成功"启用" 2. 创建守护进程用户和组 3. 将 Caddy 配置文件复制到正确位置 4. 配置 Shepherd 以后台守护进程方式运行服务 这是最小的 Caddy 服务实现——能完成工作但不够优雅。多数配置选项将硬编码,服务暂不适合分发。但它能正常工作并满足需求。完成后,我们将积累足够知识以便后续重构改进。 本文发布后计划优化服务,如果顺利甚至可能提交至 Guix 仓库。但目前先聚焦基础实现,不让完美主义阻碍进度。 ### 什么是服务? 文档将服务定义为"扩展操作系统功能的组件"。定义虽模糊,但因服务功能多样。最典型的服务是后台运行的守护进程(如本例),但服务不限于此。如后文所示,服务可以是单次执行的进程(如创建账户或复制文件到存储),也可以是周期性进程(如 cron 任务)。本质上,它是以声明式方式配置系统的原语。 服务的有趣之处在于其设计之初就支持扩展与组合。服务可以扩展一个或多个服务,也能被一个或多个服务扩展。我们将在后文深入探讨——这种架构非常出色。更多服务信息请参考[文档](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Composition.html)。 若仍不理解服务概念,这很正常。直到编写这个 Caddy 服务时我才真正理解。如果尚未完全掌握,跟着教程操作会逐渐明晰。 ## 1. 定义空服务 第一步是创建最简服务:一个无功能的空服务。目标是熟悉服务机制。 首先需定义服务类型。服务类型本质上是服务的蓝图,可类比为类与实例的关系。服务通过`service`过程"启用"或"实例化": ```scheme (service my-service-type my-service-value) ``` 若未提供值,则使用服务类型定义中的`default-value`: ```scheme (service my-service-type) ;; 使用 my-service-type 中定义的默认值 ``` 要启用服务,需将其添加到`services`过程的列表中: ```scheme (services (append (list (service my-service-type my-service-value)) %base-services)) ``` 现在查看`service-type`记录。其定义位于`gnu/services.scm:187`。服务类型文档完善,但搜索代码库在 Guix 开发中是重要工具(任何软件开发皆然!): ```scheme (define-record-type* service-type make-service-type service-type? (name service-type-name) ;符号(用于调试) ;; 被此类服务扩展的组件 (extensions service-type-extensions) ;扩展列表 ;; 给定扩展列表时如何"组合"它们 (compose service-type-compose ;列表处理函数 (默认 #f) ;; 如何将扩展组合结果附加到服务参数 (extend service-type-extend ;列表处理函数 (默认 #f) ;; 可选默认值 (default-value service-type-default-value ;任意值 (默认 &no-default-value) ;; 元数据 (description service-type-description) ;字符串 (location service-type-location ;位置信息 (default (and=> (current-source-location) source-properties->location)) (innate))) ``` 可见`service-type`需要`name`和`extensions`列表,以及可选的`compose`、`extend`、`default-value`、`description`和`location`字段。 现在定义我们的`caddy-service-type`。设置服务名为`'caddy`,将`extensions`设为 nil(空列表)[2],表示暂不扩展现有服务。`default-value`也设为 nil。暂时只关注`description`字段,它接受字符串参数用于服务描述。`compose`、`extend`和`extensions`字段将在后续详细说明,此处暂不涉及。 我们的 Caddy 服务类型定义如下: ```scheme (define caddy-service-type (service-type (name 'caddy) (extensions nil) ;; 空列表 (default-value nil) (description "Caddy 是默认使用 TLS 的可扩展服务器平台。"))) ``` 这就是最简服务类型:仅包含名称和描述。现在将其添加到`services`过程以启用: ```scheme ;; config.scm (use-modules (gnu)) (use-service-modules networking ssh) (use-package-modules screen ssh) (operating-system (host-name ...) (timezone ...) (locale ...) (bootloader ...) (file-systems ...) (users ...) (packages ...) (services (append (list (service ...) ;; 已有服务 (service caddy-service-type)) ;; ← 添加此行 %base-services))) ;; %base-services 是默认服务集 ``` 运行`sudo guix system reconfigure config.scm`即可庆祝您的首个自定义服务诞生。我们"实例化"了一个`caddy-service-type`类型的服务(未提供值,使用默认的 nil)。虽然暂未执行实际操作,但它已是一个有效服务。 若有图形界面,可运行以下命令生成系统服务图: ```bash guix system extension-graph config.scm | guix shell xdot -- xdot - ``` 您会发现 Caddy 服务已出现在服务列表中。现在让我们为这个服务添加实用功能。 ## 2. 创建守护进程用户 下一步是指示服务创建`caddy`用户和组。为后台进程创建非 root 用户是良好实践:一旦进程被攻破,攻击面将大大缩小。该用户需具备: 1. 启动守护进程的权限 2. 读取 Caddy 配置文件的权限 我们将利用`extensions`字段的威力。Guix 内置众多服务,其中包括`account-service-type`。该服务用于在操作系统中创建用户和组。要使用它,需将其添加到`caddy-service-type`的`extensions`字段。 `extensions`字段初看可能费解。我的第一反应是:它允许通过扩展自身能力来增强服务。例如预期扩展`account-service-type`会为我们的服务添加用户创建功能。但实际恰恰相反!你不是向服务"添加"能力,而是修改目标服务(此处为`account-service-type`)来为你的服务执行操作。目标服务类型的修改方式由其`compose`和`extend`字段定义,下文将详细说明。 我们先通过账户创建来说明此概念。我们要创建用户`caddy`及其同名组。`account-service-type`被定义为可扩展——即它为其他服务提供创建用户账户的接口。该服务无需知道谁在创建账户及原因;只提供创建方式。这正是架构的精妙之处:使系统完全声明式。当服务扩展`account-service-type`时,Guix 会在服务启用时创建账户。若服务未启用,账户则不会创建。这种架构令人联想到*控制反转*原则。 为使`caddy-service-type`启用时自动创建 caddy 用户,我们需扩展`account-service-type`。现在理解了`extensions`字段的概念:允许"扩展"目标服务类型以添加功能(如创建更多用户),但具体如何设置仍不明确。我们可以通过阅读文档、搜索示例或查看源码来学习。为深入理解服务机制,我们查看源码。 ### 深入探索:extend 和 compose 出于好奇,我查看`shadow.scm:547`中定义的`account-service-type`以推测账户数据结构: ```scheme (define account-service-type (service-type (name 'account) ;; 连接 , , 和骨架列表 (compose concatenate) (extend append) (extensions (list (service-extension activation-service-type account-activation) (service-extension shepherd-root-service-type account-shepherd-service) ;; 让 'user-processes' 依赖 'user-homes' ;; 确保守护进程在主目录创建后启动 (service-extension user-processes-service-type (const '(user-homes))) (service-extension etc-service-type etc-files))) (default-value '()) (description "确保指定用户账户和组存在,并创建各账户主目录。"))) ``` 可见`default-value`是空列表,表明`account-service-type`期望列表值。但列表包含*什么*?注释给出答案:``和``记录列表。若无注释,可查看`account-activation`过程的实现。 在继续之前,先说明`compose`和`extend`字段。根据[文档](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Types-and-Services.html): > **compose** > 组合多个扩展服务值的过程 > **extend** > 定义服务值如何与扩展组合结果交互的过程 这种描述略显模糊。具体含义是: 1. `Compose`定义服务如何*组合*扩展此服务类型的多个服务。例如若有三个服务扩展`account-service-type`,如何处理各自的值?仅取第一个?最后一个?显然当多个服务通过扩展定义用户时,我们需创建所有用户。因此需要连接*扩展服务*提供的值。其他服务可能有不同行为。 想象一个管理端口的服务`open-port-22-service-type`。若两个服务扩展它:一个要*开放*端口,另一个要*关闭*。应如何处理?存在冲突值。可能需要在`open-port-22-service-type`中定义规则,当收到矛盾值时优先处理"关闭"操作。这就是`compose`字段的作用:定义如何组合多个扩展服务值。 2. `Extend`定义`compose`字段的*组合结果*如何与服务自身的值交互。它与`compose`类似但针对不同层面。 想象一个计算机启动时显示欢迎消息的服务`welcome-message-service-type`。若其他服务扩展它提供不同消息,可能需要...

相似文章

切换到 GNU Guix:初学者的视角

Lobsters Hottest

一位初学者分享了从 Arch Linux 切换到 GNU Guix 的经验,讨论了声明式系统管理的好处、与 NixOS 的比较以及所面临的挑战。

Guix: 从二进制创建包

Lobsters Hottest

使用copy-build-system从二进制创建Guix包的指南,解释如何设置安装计划。

在 Emacs 中,一切都像服务

Hacker News Top

这篇文章解释了 Emacs 如何作为各种服务的客户端,利用其内置的 UI、网络通信和数据管理库,支持了“一切都像服务”在 Emacs 中的理念。