Implementing an /etc/local.d directory in systemd

(Note: This post is originally from 2020, and is copied to this new site for historical reasons. Opinions, network infrastructure, and other things may have changed since this was first written.)

Sometimes, you run a Linux system headless. Sometimes, these headless systems don’t run 24 hours a day, usually because they’re in your house and you don’t need them all the time, so you’d like to save money on your light bill. When you power these systems back on, you’ll generally want some kind of indicator to know the system is ready to do whatever it’s tasks for the day are. I like beeping PC speakers if I have them, UDP packets if I don’t.

In days of yore, when SysV init roamed the earth and Facebook required a .edu address to register an account, you would stick a beep or netcat command into /etc/rc.local and call it a day. But modern systems have foregone that script. OpenRC has a brilliant upgrade to that function; you would put a script file into /etc/local.d, appropriately named with .start or .stop at the end, depending on whether it needed to run at boot or shutdown, make it executable and call it a day.

But most distros don’t use OpenRC. Most distros use systemd, which instead replaces rc.local with…nothing. You’re expected to write a systemd unit to encompass every little oneshot script you need to run, which is a huge maintenance headache. Fortunately, however, run-parts is a mostly universally available command that does most of the legwork for us. We just need to zip tie it into systemd. Multiple service files for all these tiny scripts is a headache, but just one service file for all of them is reasonable.

There’s a key difference in implementation details here: Instead of suffixing .start/.stop at the end of your files, this method adds two subdirs to /etc/local.d named start and stop.

The service file


ExecStart=run-parts --lsbsysinit /etc/local.d/start
ExecStop=run-parts --lsbsysinit /etc/local.d/stop


Some highlights:

  • makes sure the service is started when the system boots, while should make the service run last. That way, you can be sure these scripts only run when the system is ready to work.
  • --lsbsysinit validates each script name to make sure it’s acceptable within sysinit guidelines, at least on Debian systems. I did find an old Fedora bug that hinted at them using a different implementation, and it may be true for other distros as well (that argument also tells run-parts to ignore apt cruft like .dpkg-new and similar), so you may want to remove that argument if you have issues.
  • run-parts exits its process when it’s run every script, so you want to tell systemd about that with RemainAfterExit=yes.

With that service file in place, tell systemd to use it:

# as root
$ systemctl daemon-reload
$ systemctl enable local
$ systemctl start local

An example start file

This script, assuming the above works, should cause a PC speaker to make a few nice beeps at the end of the boot process.

beep -f 500 -l 50
beep -f 750 -l 50
beep -f 1000 -l 50
beep -f 750 -l 50
  • October 21, 2022