How to write a systemd .service file running systemd-tmpfiles

[This does not directly address the issue of systemd-tmpfiles but I think you have already recognized that in this particular case you are better off just using echo.]

First up, "multi-user.target" may or may not be what you want to use. If you are familiar with the concept of runlevels from SysV style init stuff, multi-user is the systemd equivalent of runlevel 3, which is a multi-user system that boots to a console, not a GUI. The equivalent of runlevel 5, which boots to X, is graphical.target. The default is determined by a symlink in /etc/systemd/system (and/or /lib/systemd/system; the one in /etc will overrule the one in /lib) called default.target, use ls to find where it points:

»ls -l /etc/systemd/system/default.target
default.target -> /usr/lib/systemd/system/multi-user.target

systemctl get-default will tell you "multi-user.target" in this case. For normal linux desktops it will be graphical.target. This is actually not important if you want the boot service you are creating to start regardless of what the default runlevel/target is -- in that case, we can just use default.target, and not worry what it is an alias for. If you use multi-user, however, and your default is graphical, your service won't happen.

Depending on the service, there may be more appropriate and specific targets or services that you want to start this one in relation to. Based on your other question, default.target is probably fine. As a note, the difference between a "target" and a "service" is that a service contains a [Service] section which actually runs a process; a target is just a way of grouping services together via the various "depends" and "requires" directives; it doesn't do anything of its own beyond triggering other targets or services.

When a service starts is determined by what other services explicitly depend on it. In the case of a simple, stand-alone event like this that we want run late in the boot process, we can use this combination of directives:

[Unit]
Requires=local-fs.target
After=local-fs.target 

[Install]
WantedBy=default.target

The "Install" section is used when the service is installed. "WantedBy=" specifies a target we want this service to be included with, meaning it will run if that target does. If you don't have specific dependencies, getting the unit to run later rather than sooner may be a matter of looking at what's going on normally and picking something to use as a dependency or prerequisite.

To distinguish: By dependency I mean something which your unit requires to also be activated, and by prerequisite I mean something that should run before your unit if it is being used, but it is not required. Those terms are mine, but this is an important distinction used in the systemd documentation, particularly in the sense that a required dependency is guaranteed to be started if your unit is, but this requirement does not influence the order in which they are started, meaning, something that is just a dependency may actually be started afterwards (and yes, since that means your unit may be started first, the dependency is not guaranteed to succeed).

Above, Requires on local-fs.target may be a bit pointless unless you think your unit is going to be used on a system where it might not be included otherwise, but combining it with After means your unit is guaranteed to be started after it is -- so you could do without the Requires (you can set a unit to start after a unit that it doesn't depend on). The example here is just to introduce the concepts and the distinction between dependency and order of execution: One does not determine the other.

Note that "started after" still doesn't mean the prereq will have reached any particular point it its own execution. Eg., if it is about mounting remote filesystems and you this is important to your unit, you will want to use Requires and probably After the service that establishes that but you still need the actual process you are executing to do proper error handling in case the remote filesystems are not yet accessible (eg., by sleeping in a loop until they are).

For the example, I'll just echo "hello world" to the console. The service itself is described in the [Service] section:

[Service]
Type=simple
ExecStart=/usr/local/bin/helloworld

The command needs a full path. The reason I did not just use /usr/bin/echo "hello world" is that it won't work (the output goes to /dev/null, I think), and while a service that does an echo "hello world" > /dev/console will, experimentation demonstrates that using shell redirection in an ExecStart directive won't, because the ExecStart command isn't run by a shell. But you can make so: /usr/local/bin/helloworld is a shell script with that one line, echo "hello world" > /dev/console.1

Note the Type=simple. This is fine for what helloworld does, but if you have something that is going to take more than a second or so, you should fork it to the background first (eg., via & in a start-up script) and use Type=forking. The "Type" param is covered in detail in man systemd.service and you should read that part regardless of what you are trying to do.

Our complete, minimal service file is just those three sections ([Unit], [Service], and [Install]). To install, place the file or a symlink to it in either /etc/systemd/system or /usr/lib/systemd/system, and:

systemctl --system enable helloworld

It should print ln -s .... This does not run the service, it just configures it to run at boot as discussed above.

That's it in a nutshell. man systemd.unit and man systemd.service have more details (BTW, there's an index for all these things in man systemd.directives).


  1. You can redirect output using StandardOutput and StandardError parameters in the [Service] block, see man systemd.exec.

For the systemd-tmpfiles service: it should ship with your distribution, but you can always get the service file from the upstream git-repository