I've recently investigated again how to run Emacs as a systemd service. The goal is to start using the emacsclient more consistently, it's a faster way to work with Emacs.
The short version of it is: if you want to run emacs as a server without headaches, the easiest option is:
emacs --fg-daemon
then connect to the server with emacsclient -t. You're done.
Running Emacs as a systemd service is a slightly different thing and implies learning a bit about how systemd Units work. With a systemd service you can setup the complete lifecycle of an application (start, stop, restart, etc.) and have it run in a more isolated way, which is also conceptually a good thing to understand.
First I had to refresh a bit my systemd-fu. As a reference I've found a very helpful blog post from Bozhidar Batsov, which also pointed me to the invaluable ArchWiki page about systemd. These and the excellent systemd man pages were enough to figure everything out. There is also an article on the EmacsWiki but please keep in mind that (as a lot of the EmacsWiki content) is either outdated, inaccurate or just plain wrong. I usually do myself a favor and avoid the EmacsWiki.
Bozhidar points out that it's possible to use the package exec-from-shell. This package allows the user to create a "whitelist" of env variables to bring into the isolated environment, escaping the systemd "jail". I disagree with this suggestion, the correct way of importing env variables should be either using environment.d (which is also suggested) or just put them into the systemd Unit itself.
I won't repeat Bozhidar's article content, I'll instead focus on the few issues I had to solve.
§ The quirks
I have a somewhat wide usage pattern of Emacs. From Emacs I interact with a few services and external applications. This means that Emacs needs configuration that usually sits in my shell (so first ~/.profile then ~./bashrc and friends). As I mentioned, the systemd service is, by default, isolated and has access to a very restricted number of env variables (I believe from /etc/environment.d/ but didn't look into it too closely). And as I learned, systemd does not even import the XDG Base Path env variables (those starting with XDG-*).
Another quirk that I encountered is related to moving the startup of emacs to a systemd Unit: now emacs is started before the GUI user session (let's say GNOME, KDE or whatever window manager one uses) had time to fully load. I noticed that the lack of WAYLAND_DISPLAY was causing issues to my emacs, so I had to manually set it in ~/.config/environment.d/envvars.conf (as explained in the ArchWiki) and man environment.d.
All these caveats are not mentioned in any example of emacs.service I've found around, so I had to learn by myself.
§ Import XDG Base Paths
So the first thing is to map again the XDG Base Paths env variables inside the systemd Unit, my scripts configuration uses them a lot.
[Service]
Environment=XDG_CACHE_HOME=%C
Environment=XDG_CONFIG_HOME=%E
Environment=XDG_DATA_HOME=%D
Environment=XDG_STATE_HOME=%S
The ampersands are called SPECIFIERS and are Unit shortcuts to access these directories. These are described in systemd.unit(5) (under "SPECIFIERS").
As a little reminder, I modified a systemd Unit from the ArchWiki with the mapping of these directories and saved the file in $XDG_CONFIG_HOME/systemd/user/test-specifiers.service.
[Unit]
Description=Testing Unit specifiers
Documentation=man:systemd.unit(5)#SPECIFIERS
[Service]
Type=oneshot
ExecStart=printf '(XDG Base dir) = (systemd) = (envvar)\n'
ExecStart=printf 'XDG_CACHE_HOME = %%s = %%s\n' %C "${XDG_CACHE_HOME}"
ExecStart=printf 'XDG_CONFIG_HOME = %%s = %%s\n' %E "${XDG_CONFIG_HOME}"
ExecStart=printf 'Log directory root = %%s = %%s\n' %L "${XDG_STATE_HOME}"/log
ExecStart=printf 'XDG_STATE_HOME = %%s = %%s\n' %S "${XDG_STATE_HOME}"
ExecStart=printf 'XDG_DATA_HOME = %%s = %%s\n' %D "${XDG_DATA_HOME}"
ExecStart=printf 'XDG_RUNTIME_DIR = %%s = %%s\n' %t "${XDG_RUNTIME_DIR}"
# These need to be set
Environment=XDG_CACHE_HOME=%C
Environment=XDG_CONFIG_HOME=%E
Environment=XDG_DATA_HOME=%D
Environment=XDG_STATE_HOME=%S
[Install]
WantedBy=default.target
Run this with systemctl --user enable --now test-specifiers and then check the output with systemctl --user status test-specifiers: you will know if these env variables are correctly set in the context of the systemd service.
§ Inherit $HOME, $LOGNAME, and $SHELL
I soon discovered (and panicked) that I could not:
- open anymore links from org-mode files (
org-open-at-point) - open anymore links from emails (
shr-browse-url) - paste in the terminal emulator stuff from the kill ring (
kill-ring-save)
All these things were fixed by adding:
# See man systemd.exec(5)
SetLoginEnvironment=true§ The Emacs theme
I use an Emacs Doom theme which is very colourful so I need to set the terminal emulator (kitty) to handle 24 bit of color depth. In my ~/.bashrc I have:
export COLORTERM=truecolor
export TERMINFO=/usr/lib/kitty/terminfo
these become additional entries in the Unit:
[Service]
Environment=COLORTERM=truecolor
Environment=TERMINFO=/usr/lib/kitty/terminfo§ Email client
I use mu4e so I need to tell the email client where I store my emails:
[Service]
Environment=MAILDIR=%h/.local/mail
%h is another specifier that points to $HOME.
§ Auth tokens
I connect to a number of authenticated services, so I need to retrieve passwords all the time. These passwords are stored in PGP encrypted files and retrieved using the EasyPG Emacs package. These files are stored in the directory shared by pass, so it's two new entries:
[Service]
Environment=PASSWORD_STORE_DIR=%D/password-store
Environment=GNUPGHOME=%D/gnupg
As you can see these directories are under %D which points to $XDG_DATA_HOME.
With these in place, I can now fully use Emacs and reduced the context shared with the process to a fistful of env variables.
§ Useful commands
As a a reference, here some useful commands. They all have --user to indicate that the service is unprivileged (I certainly don't need to run Emacs as root).
- Enable and start a service:
systemctl --user enable --now <service>(equals to runningenableandstarta service) - Stop and disable a service:
systemctl --user disable --now <service>(equals to runningstopanddisablea service) - Show the context the service runs:
systemctl --user show-environment
Another option to import an env variable is: systemctl --user set-environment KEY=VAL but it's ephemeral and after the next reboot will be lost.
Importing all env variables with systemctl --user import-environment is deprecated and I assume will be removed at some point (understandably so because it goes against the philosophy of systemd).
§ Additional perks and gotchas of Emacs as a server
With a system Unit, if I wanted, now I could push the isolation of the Emacs process further.
After configuring the appropriate invoker, now I can run xdg-open "mailto:foo@example.com?subject=test" and quickly open a new Emacs buffer to compose an email. This is very useful to handle those mailto: links on web pages.
Interacting with Emacs via emacsclient is way faster, but I am still getting used to it, there is a change of mindset involved: being Emacs basically a "Lisp REPL" with a single huge global state. It's easy to iterate and run any Lisp code anywhere but that also means that code stays in memory until you restart Emacs.
This has subtle consequences: when I experiment with a custom piece of code I usually rename things, change things then evaluate and test. If in this process I rename a function jman/currencies to jman/retrieve-currencies to jman/retrieve-currency-rates, all three functions stay in memory unless I learn the command to unset a function definition (fmakunbound) or a variable (makunbound) or until I restart Emacs. A more visible effect if I test different themes: if I don't restart emacs between one theme and the other, the colors will mix up (!) and give me a completed wrong look.
§ Conclusions
Systemd Units are extremely powerful. With a little bit more oomph you can basically create fully containerized applications without using external solutions like Docker and with the additional benefit of learning something about how processes work.
The thing that took me some time to get right was figuring out the env variables: it was immediately apparent which env variables were needed to finally get to my usual Emacs fully working and at the same time allowing the least amount of permissions.
Let me know if you find this article useful or if you have comment.