Creating a system service with systemd
While I've got some grumblings with systemd over how it handles (or not) certain things, it's the most popular service manager on Linux systems today. By this, I mean it starts and stops the various services (like your SSH server, web server, cron) automatically, according to the rules laid out in special service files.
Since it's so popular and I keep having to write services (and look up how to do so every time), I thought I'd write a post here about it to save me the trouble :P
Bear in mind that systemd isn't the only service manager out there. Others include OpenRC, runit, upstart, and more! If you're using one of these (I'm looking to investigate using one of these on my next server rebuild), then this tutorial isn't for you. I will probably be releasing a tutorial down the road for OpenRC though, if I get around to having a server running an OS that uses it.
systemd stores it's service files in /etc/systemd/system/
, so to start we need to create a new file in there:
sudo sensible-editor /etc/systemd/system/service_name.service
With the new file open in your favourite editor, it's time to set out our service definition. This is done with an ini-like syntax. Here's an example:
[Unit]
Description=Gitea
After=syslog.target rsyslog.service network.target
[Service]
Type=simple
User=git
Group=git
WorkingDirectory=/srv/git/gitea
ExecStart=/srv/git/gitea/gitea web
Restart=always
Environment=USER=git HOME=/srv/git
[Install]
WantedBy=multi-user.target
The above is a simple service file for Gitea, which is the engine behind my personal git server. Let's go through each section one by one.
Firstly, the [Unit]
section defines the metadata about the service. It's fairly self explanatory actually - we set the description of the service here, and also the other services (space ` separated) that we want our service to be started after with the
After` property.
Next comes the [Service]
section. This section specifies how it should start the service. We tell it that it's a simple service (in other words it doesn't do anything fancy - other types are available, but we won't use them here), the user and group it should run under, and working directory of the process, and the command (and it's arguments) to execute in order to start the process.
In addition, we also tell it to restart the service if it crashes, and set a few environment variables to refine the way Gitea behaves. Very cool!
The final section, [Install]
, simply specifies the systemd-equivalent of which run-level this service should start on. It's very interesting from a how-does-my-system-work perspective - I recommend reading this Stack Exchange answer and this article for more information - it's a topic for another post here on this blog :-)
To start this new service, do the following:
sudo systemctl daemon-reload
sudo systemctl enable service_name.service
sudo systemctl start service_name.service
This starts our new service and configures it to automatically start when the system first boots.
With that taken care of, we've now got the basics down of our very own service file! We can take this further though. What if there's a secret key that we need to pass to a service on startup in an environment variable, but we don't want to specify it in the service because it's world-readable?
The answer here is a clever bit of shell scripting. Consider the following service file:
[Unit]
Description=Awesome XMPP Bot
After=network.target prosody.service
[Service]
Type=simple
User=bot
WorkingDirectory=/srv/bot
ExecStart=/srv/bot/start_service.sh
Restart=on-failure
# Other Restart options: or always, on-abort, etc
[Install]
WantedBy=multi-user.target
In this case, we've defined a service file for an XMPP bot (public server directory). In order for it to connect to an XMPP server, it needs a JID (a username - formatted like an email address) and password. However, we don't want to specify these directly in the service file because they are secret!
Instead, we've specified that it should start a shell script that's located at /srv/bot/start_service.sh
instead of the bot itself. Here's the contents of that shell script:
#!/usr/bin/env bash
source .xmpp_credentials
export XMPP_JID;
export XMPP_PASSWORD;
exec /usr/bin/mono Bot.exe
This simple shell script loads the contents of the file .xmpp_credentials
, specifies that the XMPP_JID
and XMPP_PASSWORD
environment variables should be passed to any further (sub) processes, and asks for the current process to be terminated and replaced with an instance of Mono executing our bot's code that stored in Bot.exe
(this way we don't have an extra bash
process sitting around doing nothing, since it's job is done as soon as we start the bot itself).
In this way, we can store our precious private details in a file that we can lock down so that only the bot's user account can read it. Here's what that .xmpp_credentials
file might look like:
#!/usr/bin/env bash
XMPP_JID="bot@bobsrockets.com";
XMPP_PASSWORD="sekret";
....and if I run ls .xmpp_credentials
, I might see something like this:
-r-x------ 1 bot bot 104 Nov 10 21:27 .xmpp_credentials
Here the file permissions allow only the bot
user to read and execute the file, but not modify it (sudo chown bot:bot .xmpp_credentials
and sudo chmod 0500 .xmpp_credentials
set these permissions for the curious).
These completes the tutorial on setting up services with systemd. We've seen how to create service files and make them start on boot (much easier than alternatives like running a command manually or using screen!). We've also learnt a simple way to hide credentials (though more advanced alternatives do exist).
Found this useful? Found a better way to do it? Comment below!