19.2 ExecStart, ExecStop, ExecReload: Command Directives
Alright, let’s get our hands dirty with the commands that actually do things: ExecStart, ExecStop, and ExecReload. This is the heart of your unit file, where you stop describing the service and start defining its behavior. Get this wrong, and you’ll be that person rebooting the entire server just to restart a single app. Don’t be that person.
The first thing you need to unlearn from your SysVinit days is that these directives are not just scripts you slap in. They are command lines, and systemd parses them with specific, and occasionally infuriating, rules.
The Absolute Basics: Paths and Path Searching
You specify the command to run for ExecStart. Seems simple, right? Just throw in the binary path.
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/app.py
This is the gold standard. You used an absolute path. I’m proud of you. Systemd knows exactly what to run.
Now, here’s the trap. You can do this:
[Service]
ExecStart=python3 /opt/myapp/app.py
Looks cleaner. It’s also a fantastic way to introduce bizarre, hard-to-reproduce bugs. Why? Because systemd will go searching through the $PATH defined for the service (which, by default, is a compiled-in systemd path, not your user’s shell path) to find python3. If anything changes that PATH, or a different version of Python gets installed in a directory that appears earlier in the PATH, your service subtly breaks. Just use the full path. Always. It’s not pedantic; it’s professional.
The Shell Trap: To Expand or Not to Expand
Watch this. You want to echo the start time to a logfile. Your shell brain thinks:
[Service]
ExecStart=/bin/echo "Starting at $(date)" >> /var/log/myapp.log
You reload systemd, start the service, and check the log. It says:
Starting at $(date)
Well, that was useless. systemd does not launch your command through a shell by default. It literally executes /bin/echo with the arguments "Starting, at, $(date)", >>, /var/log/myapp.log. The wildcards, variable expansions, and redirections you love and abuse in your shell are just inert strings to systemd.
You have two ways to fix this, and one is vastly superior.
The “just use a shell” method (often a bad idea):
[Service]
ExecStart=/bin/sh -c '/bin/echo "Starting at $(date)" >> /var/log/myapp.log'
This works, but now you’ve buried your actual command inside a shell string, making life harder for tools that monitor process trees. It’s a bit clunky.
The “do it properly” method (almost always better):
Use systemd’s own logging. Ditch the shell redirection entirely.
[Service]
ExecStart=/bin/echo "Starting now"
StandardOutput=journal
Or, if you must have a file, use StandardOutput=file:/var/log/myapp.log. Let systemd handle the redirection. It’s what it’s there for. This is a classic case of fighting the framework versus using it.
ExecStop: It’s Not Magic, It’s a Request
ExecStop is not a magical “force kill” command. Its job is to politely ask your daemon to shut down. systemd’s logic is: 1) Run ExecStop=. 2) Wait for the service to exit. 3) If it doesn’t exit within the TimeoutStopSec period, then and only then does it escalate to a SIGKILL.
This means your ExecStop needs to actually initiate a shutdown. For many custom apps, this is a missing feature. Let’s say your main process is a Python script. You can’t just pkill python3—that’s a sledgehammer that could kill unrelated processes.
A better pattern is to have the application manage its own PID file and offer a stop command.
[Service]
ExecStart=/opt/myapp/start-myapp.py
ExecStop=/bin/kill -TERM $(cat /var/run/myapp/myapp.pid)
PIDFile=/var/run/myapp/myapp.pid
Here, ExecStop sends a SIGTERM (the polite “please terminate” signal) specifically to the PID written by the main process. The PIDFile= directive is key here; it helps systemd track the main process and is used by utilities like systemctl status.
ExecReload: Please Don’t Just HUP Everything
The classic admin move is to send SIGHUP to a daemon to reload its config. Sometimes it works! Often it doesn’t. ExecReload lets you define exactly what “reload” means for your service.
If your application has a specific admin command for reloading, use it.
[Service]
ExecStart=/usr/bin/myapp --daemonize
ExecReload=/usr/bin/myappctl reload
This is infinitely better than blindly sending a signal. If your app truly does respect SIGHUP, you can of course do:
[Service]
ExecStart=/usr/bin/myapp --daemonize
ExecReload=/bin/kill -HUP $MAINPID
Note the use of the special $MAINPID variable. This is provided by systemd and is the preferred way to target the main process, as it’s more reliable than hoping a PID file is current.
The Type=oneshoot Conundrum
Here’s a big “gotcha.” If you set Type=oneshot because your service doesn’t daemonize (it runs and exits), you cannot use ExecStop. Why? There’s no daemon running to stop! The oneshot process has already finished. systemd considers the service “stopped” once the ExecStart process exits. In this scenario, ExecStop is ignored. If you need cleanup for a oneshot service, you have to do it as part of the main ExecStart command or, more neatly, with an ExecStopPost directive, which runs after the service is considered stopped.