Writing a Guix service from scratch, as a beginner

Lobsters Hottest Tools

Summary

The article provides a beginner-friendly guide to writing a custom service in Guix, using Caddy as an example, and covers system configuration and Scheme programming basics.

<p><a href="https://lobste.rs/s/f4arrs/writing_guix_service_from_scratch_as">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 09/14/26, 12:55 AM

# Writing a Guix service from scratch, as a beginner Source: [https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html](https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html) I am in the middle of migrating my machines from NixOS to Guix\. My first milestone is to migrate my VPS to Guix\. My VPS has essentially three majors roles: 1. host my email server\. 2. host my wireguard server\. 3. act as a proxy server to access services on my home server \(thus, hiding my home IP\)\. The mail server is up and running, using exim as the MTA and dovecot as the IMAP server\. I had to jump through a few hoops to get exim up and running\. I would have preferred postfix over exim, but only exim is available out of the box on the official Guix channel\. At the time, I didn't know how to define my own custom services\. I intend to use Guix for the foreseeable future, I want to build a deep understanding of it\. Learning to create my own services, suited to my own needs, is an important part of mastering Guix\. Which brings us to the motivation behind this article: the time had come to create my own service\. For the proxy server, I enjoy using Caddy\. Caddy isn't available as a service on Guix\.[1](https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html#fn.1) This seems like the perfect time to finally dive into custom services\. As I navigated my way through the docs and the source code, I decided to document my learning and share it here, as guide for other newcomers\. In this article, we will be writing a custom service for our Caddy reverse proxy\. This service involves setting up a configuration file for Caddy via Guix, creating a system user and configuring Shepherd to run the daemon\. It's a perfect first custom service: t is simple enough to ease into custom services, yet it includes a good overview of them\. Furthermore, most of the services I run on my home server are essentially a combination of those three actions: configure users, manage configuration files and run the daemon\. ## Who is this for? This article is suitable to Guix newcomers and beginners\. As a matter of fact, I wrote this as I went along, stumbling my way through it, without any extensive knowledge beyond reading the documentation\. ## Pre\-requirements The only true prerequisite is to have the Caddy package ready to go on your Guix instance, so we can build the service on top of it\. At the time of writing this blog, the Guix repository doesn't contain the Caddy package but the team is working on it\. In the meantime, you can follow my[blog post to create one easily](https://aloysberger.com/posts/guix-packaging-a-binary-as-a-guix-beginner.html)\. It only takes a few lines of code\. You don't need to have a high proficiency or deep expertise in Scheme \(I certainly don't\), but a basic understanding of it is highly recommended\. I assume that you are, like me, fairly new to Guix \- however this isn't a replacement to the documentation\. I will be paraphrasing the documentation a lot with my own interpretation of it\. I encourage you to keep the documentation on the side while following this article\. I obviously assume that you have Guix installed and a working configuration\. If not, you can find one[here\.](https://guix.gnu.org/manual/devel/en/html_node/Using-the-Configuration-System.html) Finally, you need to know how to reconfigure your system \(hint:`sudo guix system reconfigure my\-config\.scm`\)\. When I reference the Guix source code, I will use the notation`file\-name:line`\. ## The plan Alright, with this out the way, let's talk about what we are concretely going to build here\. In order to keep things as simple as possible, I decided to define our service to be as simple and bare\-bones as possible\. Our plan will be as follow: 1. Define an empty service, and successfully "enable" it\. 2. Create the dameon user and group\.\. 3. Copy the Caddy configuration to the right location\. 4. Configure Shepherd to run the service as a daemon in the background\. This is the bare mininum Caddy service; it will do the job, but it won't be the most elegant\. Most of the configuration options will be hard coded; the service won't be ready to be distributed\. However, it will be functional and fulfill our needs\. Once completed, we can expect to have accumulated enough knowledge along the way to revisit, refactor and improve it\. As soon as this article is out, I plan on improving it and if I feel brave, I might even propose it to the Guix repository\. But for now, let's stick to the basics\. We don't want to let perfection get in the way of done\. ### What is a service? The documentation defines a service as "[something that extends the functionality of the operating system](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Composition.html)"\. This definition is quite vague, but that's because a service can have a wide variety of functionalities\. The most obvious type of a service is a daemon running a process in the background \(our case\), but it's not the only type of service\. As we will see, a service can be a process which is run once, such as creating accounts or copying files to the store\. It can be a recurring process, such as a cron job\. In a way, it is a primitive to configure our system in a declarative manner\. One interesting aspect of services, is that they are designed from the ground up to be extensible and composable\. A service can extends one or more services, and can itself be extended by one or more services\. We will dive a deeper on that subject further below; I find this architecture fantastic\. You can read more about services in the[documentation](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Composition.html)\. If you still don't quite understand the concept of services,, it's completely normal\. It didn't click for me until I started writing this Caddy service\. So if you don't fully grasp what a service is yet, follow along, it will make more sense as we progress\. ## 1\. Define an empty service Our first step is to break the ice by creating the simplest service we can: a bare service that does nothing\. The goal is just to get familiar with services\. First, we need to define the service type of our service\. A service type is essentially a blueprint for a service\. Think of it as what a class is to an instantiated object\. A service is enabled or "instantiated" \(if we keep our analogy of classes\) by the`service`procedure\. The`service`procedure takes as an argument a`<service\-type\>`record and a value\. ``` (service my-service-type my-service-value) ``` If a value isn't provided, the`default\-value`defined in the`<service\-type\>`definition is used\. ``` (service my-service-type) ;; this service is using the default-value defined in <my-service-type> ``` To enable the`service`, we add it to the list provided to the`services`procedure: ``` (services (append (list (service my-service-type my-service-value)) %base-services)) ``` Now let's look inside the`<service\-type\>`record\. You can find the definition of`<service\-type\>`in`gnu/services\.scm:187`\. The service type is well documented, but grepping the codebase is very valuable tool when working with Guix \(or any other software if you ask me\!\)\. ``` (define-record-type* <service-type> service-type make-service-type service-type? (name service-type-name) ;symbol (for debugging) ;; Things extended by services of this type. (extensions service-type-extensions) ;list of <service-extensions> ;; Given a list of extensions, "compose" them. (compose service-type-compose ;list of Any -> Any (default #f)) ;; Extend the services' own parameters with the extension composition. (extend service-type-extend ;list of Any -> parameters (default #f)) ;; Optional default value for instances of this type. (default-value service-type-default-value ;Any (default &no-default-value)) ;; Meta-data. (description service-type-description) ;string (location service-type-location ;<location> (default (and=> (current-source-location) source-properties->location)) (innate))) ``` We notice that the`<service\-type\>`requires a`name`and a list of`extensions`\. It also contains optional fields:`compose`,`extend`,`default\-value`,`description`and`location`\. Let's define our`caddy\-service\-type`\. We set the name of our service to`'caddy`\. We also set the`extensions`to[nil](https://doc.guix.gnu.org/guile/latest/en/html_node/Nil.html)\(the empty list\), which means that we aren't extending any existing services for now\. We also leave the`default\-value`to`nil`\. Regarding the optional fields, let's only care about the`description`for now\. It takes a`string`and is used for \- you guessed it \- the description\. I will cover the`compose`,`extend`and`extensions`fields later, let's not worry about them for now\. Our`<service\-type\>`for our Caddy service looks like this: ``` (define caddy-service-type (service-type (name 'caddy) (extensions nil) ;; an empty list (default-value nil) (description "Caddy is an extensible server platform that uses TLS by default."))) ``` That's it\. This is the simplest service type: it only has a name and a description\. Let's enable it by adding it to the`services`procedure\. ``` ;; 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 ....) ;; Pre-existing services (service caddy-service-type)) ;; <-- Add this line here %base-services))) ;; %base-services are the default services ``` Now, run`sudo guix system reconfigure config\.scm`and celebrate your first custom service\. We "instantiated" a service of type`caddy\-service\-type`without a value, defaulting to the default\-value of`nil`\. It's not doing anything, but it still is a service\. If you have a graphical display, you can run the following command to generate a graph of your system: `guix system extension\-graph config\.scm \| guix shell xdot \-\- xdot \-` You will notice that the Caddy service is present in your services\. Nice\. Now let's make that service do something useful\. ## 2\. Create the daemon user The next step to improve our service is to instruct it to create the`caddy`user and group\. It is always a good idea to create a non\-root user to run a background process: if the process is compromised, the area of attack is much smaller\. This user will: 1. Start the daemon\. 2. Have read access to the Caddy configuration file\. In order to create the user and group, we will use the power of`extensions`\. Guix contains a large number of services out of the box; one of them is the`account\-service\-type`\. This service is used to create users and groups on our operating system\. In order to use that service, we will need to add it to the`extensions`field of our`caddy\-service\-type`\. The`extensions`field is a bit hard to grasp at first\. My first instinct was that the`extensions`field allows you to modify your service by extending its own capability, giving it extra functionality\. For example, in this case, I expected that extending the`account\-service\-type`would add the capability to create users and groups to our`caddy\-service\-type`\. However it is the opposite\! You don't "add" capability to your service, you modify the target service \(`account\-service\-type`here\) to perform actions for your service\. How the target service\-type is modified is defined by the rules in its`compose`and`extend`fields\. We'll go over them in more details further below\. For now, let's continue on our account creation to illustrate this concept\. We want to create a user,`caddy`, and a group with the same name,`caddy`\. The`account\-service\-type`is defined as extendable\. This means that it offers an interface for other services to create user accounts\. The`account\-service\-type`doesn't need to know who is creating accounts and why; it only exposes a way to do so\. This is the beauty of this architecture: it makes the system completely declarative\. When a service extends the`account\-service\-type`, then Guix will create the accounts when the service is enabled\. If the service is not enabled, those accounts aren't created\. It's a fantastic architecture when you think about it, it reminds me of the principle of*Inversion of Control*\. In order to instruct the`account\-service\-type`to create our caddy user when the`caddy\-service\-type`is enabled, we extend the`account\-service\-type`\. We now understand the concept of the`extensions`field\. It allows you to 'extend' a target service\-type to, for example, create more users\. But we still don't really know how to specify it in practice\. We have a few ways of finding out set it up: reading the docs, grepping examples, or looking at the source\. For the sake of getting better at understanding services, let's look at the source\. ### Deeper dive: extend and compose Being curious and eager to really assimilate how services extensions work, I tried find out if we can guess the shape of the accounts by looking at the`account\-service\-type`service, defined in`shadow\.scm:547`\. This is how the`account\-service\-type`is defined\. We should be familiar with the`<service\-type\>`record structure\. ``` (define account-service-type (service-type (name 'account) ;; Concatenate <user-account>, <user-group>, and skeleton ;; lists. (compose concatenate) (extend append) (extensions (list (service-extension activation-service-type account-activation) (service-extension shepherd-root-service-type account-shepherd-service) ;; Have 'user-processes' depend on 'user-homes' so that ;; daemons start after their home directory has been ;; created. (service-extension user-processes-service-type (const '(user-homes))) (service-extension etc-service-type etc-files))) (default-value '()) (description "Ensure the specified user accounts and groups exist, as well as each account home directory."))) ``` The first thing we can see, is that the`default\-value`is an empty list, which means that the`account\-service\-type`itself is expecting a list as a value\. But a list of*what*? In this case, the comments give us the answer: a list of`<user\-account\>`and`<user\-group\>`\. If it was not for the comments, you could look at the`account\-activation`\. Before moving on, let's talk about`compose`and`extend`fields\. This is how they are defined in[the documentation](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Types-and-Services.html) > \> compose This is the procedure to compose the list of extensions to services of this type\. > \> extend This procedure defines how the value of the service is extended with the composition of the extensions\. This is a bit vague and left me a bit scratching my head, but what it means is that: 1. `Compose`defines how to the service*composes*multiple services*extending*this service\-type\. For example,if we have three services listing`accounts\-service\-type`as an extension, how do we handle each value? Do we only take the first? The last? Obviously, if three services are defining users by extending the`account\-service\-type`, we want to create all those users\. Therefore, we want to concatenate the values provided by the*extending*services\. We can imagine other services behaving differently\. For example, let's imagine a service whose role is to manage a port\. Let's call it`open\-port\-22\-service\-type`\. Let's also imagine that two services extend this service: one wants to*open*the port, the other*close*it\. What shall we do? Open it, or close it? We have conflicting values\. Maybe we want to err on the caution side and define`open\-port\-22\-service\-type`to prioritise 'close' values if it receives contradicting values\. That's what the`compose`field is for\. It defines how to`compose`multiple extending service values\. 2. `Extend`defines how the*composition*from the`compose`field interact with the service's value itself\. It is, in a way, quite similar to`compose` Let's imagine a service\-type which displays a welcome message when the computer boots, called`welcome\-message\-service\-type`\. If a service extends it with a different message, we might want to give priority to the extending service value\. In this case, we would use`extend`to instruct the`welcome\-message\-service\-type`to replace its own value with the composed values from the extending services\. Note that`compose`and`extend`accept procedures which takes the service itself as an argument\. It gives the user a lot of freedom and power when defining the rules\. Let's finish with the`user\-account\-service\-type`as a final example\. In the case of users, we want to: 1. `compose`: concatenate the values to create all the users and groups received\. 2. `extend`: append to the service value\. so we create all the users and groups received AND the ones specified by the`user\-account\-service\-type`service itself \(remember that we can instantiate the service with a value\)\. Zooming back to the architecture, we can see that services are all built upon each other\. A service, when installed, will modify the services listed in its`extensions`field\. Those will in turn modify the service\-type of their own extensions\. This "chain reaction" will continue until all services are collected in the`system\-service\-type`, which is the root of all services\. Under the hood, we end up with a collection of all our services in one, large, single structure\. I assume that this is conglomerate of services is then handled by Guix when configuring the system\. Ok, we now know the`account\-service\-type`: 1. Accept a list of`<user\-account\>`and`<user\-group\>`records 2. Concatenate any values received as per its`compose`rule\. 3. Append the \(concatenated\) received values to its own value, as per the`extend`rule\. ### Back to our <user\-account\> and <group\-account\> So, by either following in my 'deep dive', reading the documentation or looking up examples in the code base, we found out that our group and user are defined as a`<user\-account\>`and`<user\-group\>`respectively\. Both records can be found in`accounts\.scm`at line 71 and 90 respectively\. We define our accounts like so: ``` (define %caddy-accounts (list (user-account (name "caddy") (group "caddy") (comment "Run the caddy daemon") (system? #t)) ;;<-- the caddy user is a system user, not a real user (user-group (name "caddy") (system? #t)))) ``` Now we can add them the`account\-service\-type`extension\. The`extensions`procedure accepts a list of`<service\-extension\>`\. A`service\-extension`is a record that accept two parameters\. The first is the name of the target`service\-type`\. This is`account\-service\-type`in our case\. The second parameter to a`service`\-extension is a procedure that takes the service itself \(the current caddy service\) and returns a list of object to extend the target service \([see documentation](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Types-and-Services.html)\)\. The second parameter seems complicated, but it's simply a procedure that returns the "value" for the extension\. In our case, a list of user and group account\. You can imagine that using this second argument, we could create the user dynamically based on the value of our`caddy\-service\-type`if we wanted make our service configurable\. That's how configurable services are structured\. Let's define our service\-extension for the`account\-service\-type`and add it to the`extension`field\. ``` (define caddy-service-type (service-type (name 'caddy) (extensions (list (service-extension account-service-type (const %caddy-accounts)))) (description "Caddy is an extensible server platform that uses TLS by default."))) ``` We used`const`here, which is the syntactic sugar for`\(lambda\(\_\)\(%caddy\-accounts\)\)`\. Alright, time to finally test it out\! Run`sudo guix system reconfigure config\.scm`to apply our changes\. Let's check the presence of the caddy user by running`sudo id caddy`\. ``` > sudo id caddy # uid=982(caddy) gid=976(caddy) groups=976(caddy) ``` Nice\! Our`caddy`user and group were created\! What we have learned so far: - Services are 'things' that modify the system\. - Services can be extended and can extend other services\. - Services define how they can be extended, via the`compose`and`extend`fields\. - Services define the service they extend, via the`extensions`field\. - All services are eventually "collected" in the`system\-service\-type`, the root service\. - We have learned to how to extend the`account\-service\-type`to create a user and a group for our service\. - We have also learned to dig around the source code to learn more about services\. Now let's see how we can write the config, the Caddy file, into`/etc/caddy`\. ## 3\. Copy the caddy configuration file to the right location In order to keep matters simple, the`caddy\-service\-type`will be static\. We will write the`Caddyfile`manually and provide it to our Caddy service\. We won't be in charge of generating the`Caddyfile`[2](https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html#fn.2)\. The Caddy service will simply be in charge of copying it to the right location\. The target location for`Caddyfile`is`/etc/caddy/caddy\.conf`\. As before, we will be leveraging extensions to do so\. To execute an arbitrary procedure when a service is enabled, we can use the`activation\-service\-type`\. Note that there is a service doing exactly this task, the`etc\-service\-type`\. However I believe that using the more general`activation\-service\-type`for didactic purpose is a better choice\. It's a widely used service; a lot of the service in the Guix repository rely on it\. The activation service is a service that is executed at activation of our service\. Interestingly enough, I couldn't find much about it in the documentation, except in[this example\.](https://guix.gnu.org/manual/1.5.0/en/html_node/Service-Types-and-Services.html)Let's look at the source code instead\. In`services\.scm:792`we get a bit more information: ``` > ;; The activation service produces the activation script from the gexps it ;; receives. ``` We can also look at its definition`services\.scm:776` ``` (define activation-service-type (service-type (name 'activate) (extensions (list (service-extension boot-service-type gexps->activation-gexp) (service-extension system-service-type activation-profile-entry))) (compose identity) (extend second-argument) (default-value #f) (description "Run @dfn{activation} code at boot time and upon @command{guix system reconfigure} completion."))) ``` Alright, we see \(from the comment and the lambda`gexps\-\>activation\-gexp`\) that the value required by a service is a**G\-Expression**or**gexp** What is a gexp? A gexp, or g\-exp, or G\-expression[3](https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html#fn.3)is a Guix of staging code to be executed later in the build environment\. If you know what a G\-expression is, you can skip the next section\. If not, let's briefly introduce the concept of G\-expression[4](https://aloysberger.com/posts/writing-a-guix-service-from-scratch-as-a-beginner.html#fn.4)\. ### Shallow dive: G\-exps I won't go to deep into the concept of G\-exps; there is a lot to cover and it merits its own article\. We will keep a high level overview\. To understand what G\-expression are, you need to know that Guix runs in two environments\. When building packages, the guix daemon creates something akin to containers\. They aren't quite containers, but the concept is close enough for our understanding\. To ensure reproducibility, each package is built in its own "container", which only includes the required inputs\. For this to happen, part of the code in package definition needs to be staged for later execution, inside that "container"\. G\-expressions is how we staging code to be executed at build time\. The two environments of Guix are often referred as: 1. The "host" environment, in which we define packages and configurations \(what we are doing here\)\. 2. The "build" environment, in which the Guix daemon is performing the actual build actions inside the "container"\. G\-expressions are expressed with the`\#~`macro, such as`\#~\( ;; this is a g\-expression ;; \)`\. Whenever you see the macro`\#~`, it means that the code inside the bracket will be executed later, inside the build "container"\. Inside a G\-exp, we can evaluate some expressions using the`\#$`macro, or`ungexp`\. For example, let's have a look at this G\-expression: ``` #~(begin (mkdir #$output) (mkdir "/another/folder/path")) ``` In this instance, when the code is evaluated`\#$output`will be replaced \(or`ungexp`\) and the expression will be: ``` #~(begin (mkdir "/the/output/dir") (mkdir "/another/folder/path")) ``` In concept, it's similar to the quasi\-quote and comma in Scheme\. As the G\-expression is evaluated in a different environment, it will not have automatically access to the modules imported in the current environment\. In order to give the G\-expression access to modules, we need to import them in the "build" environment in which the G\-expression will be evaluated\. To do so, we use the`with\-imported\-modules`syntax like so: ``` (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (mkdir-p "/the/output/dir") ;;mkdir-p is a utility found in guix build utils. (mkdir-p "/another/folder/path"))) ``` In the example above,`mkdir\-p`is a procedure from in`guix/build/utils\.scm`\. The`with\-imported\-modules`provides the modules to the G\-expression environment\. We can then use the standard`use\-modules`syntax as we would in a normal Scheme environment\. There is a lot more to G\-expressions, but it merits its own article so I won't go any further\. This is all we need to know at this stage: 1. G\-expression is code staged to be executed in the "build" phase of Guix\. 2. G\-expression are written with the`\#~`macro\. 3. We can`ungexp`part of the G\-expression with the`\#$`macro\. 4. We can import modules into the G\-expression with the`with\-imported\-modules`syntax\. If you want to dig deeper, some excellent resources are: - [G\-Expressions in the documentation](https://guix.gnu.org/manual/1.5.0/en/html_node/G_002dExpressions.html) - [FOSDEM Talk about G\-exps](https://www.youtube.com/watch?v=XdxYsdFOLd)\(video\) - [Code Staging in GNU Guix](https://arxiv.org/abs/1709.00833)\(paper, but very accessible\) ### Back to the activation service With this newly acquired knowledge, let's think about what we are trying to achieve here\. We want to: 1. Create the`/etc/caddy`directory\. 2. Copy the configuration file at`/etc/caddy/caddyfile`\. 3. Ensure that the configuration file is read\-only for the`caddy`user\. We need to express those three instructions in a G\-expression that we will pass as an argument to the`service\-extension`for the`activation\-service\-type`\. This is how we can do so: ``` (define %caddyfile (plain-file "CaddyFile" ":8080 { respond \"Hello world!\"}")) ;; Generate a plain caddyfile configuration. ;; Refer to https://caddyserver.com/docs/caddyfile-tutorial for more information (define (%caddy-activation _) (with-imported-modules '((guix build utils)) ;; we need some utilities, such as `mkdir-p #~(begin (use-modules (guix build utils)) (mkdir-p "/etc/caddy") ;; 1. Create the directory (copy-file #$%caddyfile "/etc/caddy/caddyfile") ;; 2. Copy the configuration file at the right location (notice the ungexp!). (chown "/etc/caddy/caddyfile" (passwd:uid (getpwnam "root")) ;; 3. Change the ownership to root:caddy. (group:gid (getgrnam "caddy"))) (chmod "/etc/caddy/caddyfile" 750)))) ;; 4. Set permissions. ``` You notice that the`%caddy\-activation`procedure accepts an argument\. As discussed earlier, it can receive the current`service`as an argument, allowing you to dynamically modify the output\. For example, you could imagine giving the`caddy\-service\-type`the ability to configure the`caddy`username\. Ok, let's try it out: ``` (define caddy-service-type (service-type (name 'caddy) (description "Run Caddy, the simple proxy server.") (extensions (list (service-extension account-service-type (const %caddy-accounts)) (service-extension activation-service-type %caddy-activation) ;; <- Adding the extension here. (default-value nil))) ``` Now run`sudo guix system reconfigure config\.scm`\. Inspect the content of`/etc/caddy/caddyfile`: ``` > sudo cat /etc/caddy/caddyfile # :8080 { respond "Hello world!"} ``` Great, we now have our user and our configuration file\. We can test out our service by starting`caddy`manually\. ``` sudo caddy -c /etc/caddy/caddyfile" # you need sudo here, we will resolve this in part 4. ``` In a second terminal, run`curl localhost:8080`\. You should obtain the response`"Hello world\!"`\. We now have successfully created a service to run caddy\! This concludes the configuration of our service\. Our service: 1. creates the daemon user and group\. 2. generate the caddy configuration file 3. copy it to the right location\. The last part is about making it run as a daemon managed by Shepherd in the background, so we don't need to start it manually\. ## 4\. Configure shepherd to run the service in the background \(*shepherd service*\)\. We now have a working Caddy service\. The last remaining part to this guide is to modify our service to instruct Shepherd to run it in the background\. We should by now have a hunch of what needs to be done: we need to extend the Shepherd service\. Before we do this, we need to do a final detour and talk about privileged ports\. ### Privileged users and privileged programs On most linux distributions, including Guix, any port numbers below 1024 are special: standard users are not allowed to bind process to them\. Since our`caddy`user is not a privileged user, it won't be able to run the Caddy server on port 80 \(http\) and 443 \(https\)\. We need to allow the Caddy process to be run on those ports\. The way to go about this in Guix is to use[privileged programs](https://guix.gnu.org/manual/devel/en/html_node/Privileged-Programs.html)\. To make Caddy a privilived program, we append it to the`%default\-privileged\-programs`and assign it the necessary extra capabilities\. ``` (privileged-programs (append (list (privileged-program (program (file-append caddy "/bin/caddy")) (capabilities "cap_net_bind_service=ep"))) %default-privileged-programs)) ``` If you're extra curious, you can take a peek at the list of`privileged\-programs`in`system\.scm:1267`\. It contains a lot of well known utility requiring extra privileges, such as`sudo`,`su`,`passwd`,`ping`or`mount`\. Here, we are adding the capability**CAPNETBINDSERVICE**as described in[man 7 capabilities](https://linux.die.net/man/7/capabilities)as*Bind a socket to Internet domain privileged ports \(port numbers less than 1024\)*\. The`=ep`means that capability is \*e\*ffective and \*p\*ermitted, essentially giving it the capability there and then\. An important thing to note: when we make a program privileged, it is relocated outside of the store\. The store cannot contain privileged for security concerns, you can[read more about it here](https://guix.gnu.org/manual/1.5.0/en/html_node/Privileged-Programs.html)\. The privileged programs are located in`/run/privileged`\. Our 'privilged'`Caddy`program is located at`/run/privileged/bin/caddy`\. ### Configuring Shepherd The next step is to configure Shepherd\. To do so, we need to extend the`shepherd\-root\-service\-type`with a`shepherd\-service`record\. In this case, I won't go into too much detail\. The[Shepherd service documentation](https://guix.gnu.org/manual/devel/en/html_node/Shepherd-Services.html)is very helpful and detailed, I highly recommend you to read it\. In particular the page about[the page about service constructor and destructors](https://doc.guix.gnu.org/shepherd/latest/en/html_node/Service-De_002d-and-Constructors.html)explains the role of`make\-forkexec\-constructor`and`make\-kill\-desstructor`, both doing a lot of the heavy lifting here\. Instead, I will comment the intentions on the extension itself: ``` (define (%caddy-shepherd-service _) ;; We ignore the config argument as we will not use it. (list (shepherd-service (provision '(caddy)) ;; this is purely an arbitrary naming handle. (documentation "Run the caddy daemon") ;; self explanatory (requirement '(networking user-processes)) ;; We instruct shepherd to start the caddy daemon after the networking and user modules. (start #~(make-forkexec-constructor ;; the command to start the process: note the use of g-exps! Refer to the doc for an exhaustive list of options (list "/run/privileged/bin/caddy" ;; Note the location of our privileged version of caddy. The "run" commands and its arguments are Caddy related; refer to the caddy documentation "run" "--config" "/etc/caddy/caddyfile" "--adapter" "caddyfile") #:user "caddy" ;; The user and group in charge of the daemon #:group "caddy" #:log-file "/var/log/caddy.log" #:environment-variables ;; Required for the logs '("HOME=/var/lib/caddy"))) (stop #~(make-kill-destructor))))) ;; The destructor, called when the process is killed. Refer to the documentation ``` A few point worth mentioning: - `'user\-processes`is a requirement for most`shepherd\-service`as mentioned in the[documentation](https://guix.gnu.org/manual/1.5.0/en/html_node/Shepherd-Services.html)\. We need the file systems for the user to be mounted before starting Caddy\. - `'networking`is also set as a requirement, we want the network stack up before we start`caddy`\. I found this while looking at how the exim service was written\. As far as I can tell, these aren't documented\. You will need to look directly in the source code\. - I don't quite recall why I set up the environment variable, but I believe it was a requirement for the logs\. The service would otherwise it would complain\. It is[mentionned in the Caddy documentation when using systemctl](https://caddyserver.com/docs/running)\. ## Final code This is how our entire service looks like: ``` (use-modules (gnu) (guix packages) (guix utils) (guix download) (guix licenses) (guix gexp) ;;; Redacted ... many modules ) (use-service-modules ;; Redacted... many modules desktop networking) ;; Our caddy package, as seen in a previous article (define caddy (package (name "caddy") (version "2.11.4") (source (origin (method url-fetch) (uri (string-append "https://github.com/caddyserver/caddy/releases/download/v" version "/caddy_" version "_linux_amd64.tar.gz")) (sha256 (base32 "1fbvxj6mifdqhwm5s1f8snr805k0anllzlri7cg9l61rgj8vyzsj")))) (build-system copy-build-system) (arguments (list #:install-plan #~'(("caddy" "bin/")))) (home-page "https://caddyserver.com") (synopsis "Web server with automatic HTTPS") (description "Caddy is an extensible web server with automatic TLS.") (license asl2.0))) ;; Our caddy user:group for the daemon (define %caddy-accounts (list (user-account (name "caddy") (group "caddy") (system? #t) (comment "Caddy daemon user") (home-directory "/var/lib/caddy") (create-home-directory? #t) (shell (file-append shadow "/sbin/nologin"))) (user-group (name "caddy") (system? #t)))) ;; Our Caddyfile (define %caddyfile (plain-file "Caddyfile" "aloysberger.com { root * /srv/http/blog file_server } :8080 { respond \"Hello, World!\" }")) ;; There is more to my Caddyfile, but yes, it serves this blog! ;; The activation to copy the Caddy file (define (%caddy-activation _) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (mkdir-p "/etc/caddy") (copy-file #$%caddyfile "/etc/caddy/caddyfile") (chown "/etc/caddy/caddyfile" (passwd:uid (getpwnam "root")) (group:gid (getgrnam "caddy"))) (chmod "/etc/caddy/caddyfile" 750)))) ;; The Shepherd definition for our Caddy daemon (define (%caddy-shepherd-service _) (list (shepherd-service (provision '(caddy)) (documentation "Run the caddy daemon") (requirement '(networking)) (start #~(make-forkexec-constructor (list "/run/privileged/bin/caddy" "run" "--config" "/etc/caddy/caddyfile" "--adapter" "caddyfile") #:user "caddy" #:group "caddy" #:log-file "/var/log/caddy.log" #:environment-variables '("HOME=/var/lib/caddy"))) (stop #~(make-kill-destructor))))) ;; And finally, the entire service (define caddy-service-type (service-type (name 'caddy) (description "Run Caddy, the simple proxy server.") (extensions (list (service-extension account-service-type (const %caddy-accounts)) (service-extension activation-service-type %caddy-activation) (service-extension shepherd-root-service-type %caddy-shepherd-service))) (default-value #t))) (operating-system (locale "en_AU.utf8") (timezone "Australia/Brisbane") (keyboard-layout (keyboard-layout "au")) (host-name "[REDACTED]") (users (cons* (user-account ;; [REDACTED - Users are defined here %base-user-accounts)) ;; Elevating Caddy's privilege to bind to port under 1024. (privileged-programs (append (list (privileged-program (program (file-append caddy "/bin/caddy")) (capabilities "cap_net_bind_service=ep"))) %default-privileged-programs)) (services (append (list (service ;; [REDACTED] Many services here: SSH, paperless, mail... (service caddy-service-type) ;; <-- Our Caddy service! %base-services)) (bootloader (bootloader-configuration (bootloader grub-bootloader) (targets (list "[REDACTED]")) (keyboard-layout keyboard-layout))) (swap-devices (list (swap-space (target (uuid "[REDACTED"))))) ``` Alright, time to start it:`sudo guix system reconfigure config\.scm` Once started, we can check the health of our daemon using the`herd`command:`sudo herd status caddy`\. You should see something like so: ``` Status of caddy: It is running since Wed Jul 8 18:06:04 2026 (41 days ago). Main PID: 17241 Command: /run/privileged/bin/caddy run --config /etc/caddy/caddyfile --adapter caddyfile It is enabled. Provides: caddy Requires: networking Replacement pending (restart to upgrade). Will be respawned. Log file: /var/log/caddy.log ``` Let's verify it works: ``` > curl localhost:8080 # Hello, World! ``` Nice\. Our service is working and the process is running in the background, managed by Shepherd\. ### How to modify the Caddyfile If you wish to change the Caddyfile, you can modify our`%caddyfile`and reconfigure the system\. Alternatively, for quick debugging purpose, you can modify the file directly at`/etc/caddy/caddyfile`and restart the caddy server with`sudo herd restart caddy`\. ## Conclusion If you have read thus far, thank you\! As a summary, let's review what we have covered: - Guix services and the extensions architecture\. - executing an arbitrary command when enabling a service \(`activation\-service\-type`\)\. - creating and managing users declaratively \(`user\-account\-service\-type`\)\. - a \(quick\!\) primer on G\-expressions\. - elevating program privileges with`privileged\-programs`, - running a service as a daemon in the background using Shepherd \(`shepherd\-service\-type`\)\. As stated in the title, I am not an expert in Guix or Scheme\. I stumbled my way through this creating this service; and it probably show\! In any case, I hope that it will help you getting start with custom services and that it clarified some concepts around services\. I am sure there are improvements to be made, such as using the`etc\-service\-type`instead of the`activation\-service\-type`\. If you have any comments, please kindly send them to contact@aloysberger\.com\. I hope you enjoyed this article\.

Similar Articles

Switching to GNU Guix: A Beginner's Perspective

Lobsters Hottest

A beginner shares their experience switching from Arch Linux to GNU Guix, discussing the benefits of declarative system management, comparisons with NixOS, and challenges faced.

In Emacs, Everything Looks Like a Service

Hacker News Top

The article explains how Emacs can function as a client to various services, leveraging its built-in libraries for UI, network communication, and data management, supporting the idea that 'everything looks like a service' in Emacs.

Guix shell now inside Emacs

Lobsters Hottest

This post describes a new `guix shell` command interface inside Emacs contributed to the emacs-guix package, allowing users to create Guix environments without leaving Emacs.