Skip to main content
  1. Posts/

Self-hosting a Cooklang Server via systemd

·977 words·5 mins·
Author
Nick Dumas
Table of Contents

Cooklang
#

Recently I became aware that cooklang’s CLI has a built in server subcommand, giving you a web interface for viewing your recipes and generating shopping lists.

Up until now, I’ve kept my recipes in Obsidian which is great but Markdown has its limitations. The value-add for using Cooklang and its builtin web server is that I can easily build shopping lists for recipes and share them with friends and loved ones with a simple direct link.

The systemd unit
#

Starting the cooklang server is very straightforward: cook server, or cook server path/to/recipes/. This isn’t very sustainable long-term, though, we don’t want to be responsible for manually restarting the server process when it crashes for example. To handle that, we’ll use a process supervisor which on most modern Linux systems will be systemd by default.

systemd uses unit files to configure resources it’s responsible for. There’s a lot going on in unit files so I’ve added as many comments as I could to explain what each lines does. Here, we’re configuring a service which specifically describes a process that systemd is responsible for maintaining.

[Unit]
# Describe your unit. This is displayed in various system utilities for human administrators to understand what they're looking at.
Description="cooklang web server"
# The After parameter can be repeated and tells systemd that this unit file MUST NOT be started until the named resources are successfully made available.
# The syslog target guarantees that logs emitted by the cooklang server can be collected.
After=syslog.target
# The network target is available when the OS's network stack comes up. Being a web server, it doesn't really make sense to start until we can talk to other computers.
After=network.target

[Service]
# This throttles how quickly the service will be restarted. systemd will only attempt to restart the cooklang server once every 2 seconds. For applications that induce heavy load on the host machine during startup, this can be critical for preventing death spirals or otherwise degrading the performance of other services.
RestartSec=2s
# This is probably the most complex parameter in the whole file. For most processes, simple will do. I recommend reading through the documentation ( https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Type= ) for more info.
Type=simple
# Tells systemd what user the process should be started as. Remember, anything that communicates with the outside world should be run as a standalone user with minimal priveleges; if the process is compromised, this limits the scope of damage malicious actors can cause.
User=cook
# Similar to the user parameter. UNIX groups allow different users to share access to a limited set of resources.
Group=cook
# WorkingDirectory effectively tells systemd to cd into the specified directory before executing the process.
WorkingDirectory=/home/cook/recipes/
# Here we tell systemd how to actually start the supervised process.
ExecStart=cook server
# The Restart parameter tells systemd how to handle restarting the process when it fails or is asked to restart by an operator. always means "Ignore exit codes and other signals and restart the process when asked, no matter what happens". For details on the other options, see https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Restart=
Restart=always

[Install]
# WantedBy is part of systemd's dependency-chain configuration. multi-user.target is marked as available when the kernel/OS pass from single-user mode to multi-user mode, effectively meaning that the system is ready to start doing stuff other than bootstrapping the hardware and kernel. In short, "when the machine boots up, start me".
WantedBy=multi-user.target

With this unit file in place, my recipes site comes to life, with the help of a new block in my caddyfile:

cook.ndumas.com {
    encode gzip
    reverse_proxy localhost:9080
}

Cooklang can’t handle new recipe files
#

Unfortunately, there’s a catch. It looks like the cooklang server can’t gracefully handle the addition of new recipes; the UI will display the recipe name but throw an error about invalid JSON when you attempt to navigate to that recipe’s page. This seems like a pretty egregious bug/oversight but luckily, systemd is exceedingly clever and already has a solution for this baked in. Using systemd path units, you can tell systemd to perform actions based on the existence or modification/deletion of specified files or directories.

The full solution involves creating two additional unit-files: cooklang-watcher.service and cooklang-watcher.path. As above, I’ll annotate the (new) directives to explain their functionality.

cooklang-watcher.service
#

[Unit]
Description=cooklang server restarter
After=network.target
# The next two parameters work in tandem to prevent the service file from being triggered too many times, whatever that may mean for a given process. During each span of 10 seconds, systemd will only attempt to restart the process 5 times. Much like Restart in cooklang.service, this is to prevent death-spiraling and degradation of other processes on the host.
StartLimitIntervalSec=10
StartLimitBurst=5

[Service]
# oneshot is similar to simple but it considers the unit "up"/available after the process exits. Use this when the process you're running is not meant to persist.
Type=oneshot
ExecStart=/usr/bin/systemctl restart cooklang.service

[Install]
WantedBy=multi-user.target

cooklang-watcher.path
#

[Unit]
Description="Monitor /home/cook/recipes for changes"

[Path]
# This tells systemd which service to trigger when the configured file events are detected. Here, we're telling it "Trigger cooklang-watcher.service when you detect changes to /home/cook/recipes"
Unit=cooklang-watcher.service
# PathModified is one of several subtle variations that tell systemd what directory/file to watch and what type of events to care about. PathModified catches more events than PathChanged which is important because I'll frequently be editing these files interactively in vim as opposed to say, a CI/CD system which will do big batches of simple writes.
# For more info, see the docs https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html#PathExists=
PathModified=/home/cook/recipes

[Install]
WantedBy=multi-user.target

Conclusion
#

This is by far the most advanced systemd setup I’ve rolled from scratch and I’m really pleased with how it came together. I don’t know if I’ll stick with cooklang long-term but from an administrator/operator perspective the tools offered everything I needed to handle all the edge cases. Now I can start converting my recipes from Markdown.

Related

The Gallery and the Toolbox
·900 words·5 mins
Note-taking can present an overwhelming abundance of possibility. Developing explicit mental models of your notes can grant clarity when organizing your knowledge.
How to find that one volume you're pretty sure you didn't lose
·430 words·3 mins
Docker volumes can be opaque, so I wrote a small bash script to help you troubleshoot.
Too Many Games 2024 Retrospective
·1516 words·8 mins
In which I drive to Philadelphia and try to play a bunch of games.