Build a service
Choose a service type
Choose Type=simple, exec, notify, forking, or oneshot from the process behavior you actually control.
The service Type= tells systemd two things. When did startup succeed? And which process is the main one? Pick it from how your program behaves, not from whatever you copied last time.
The types that matter
Type=simple is the default. systemd considers the service started the instant it forks the process, before your binary even runs. That has a strange consequence: systemctl start can report success for a service whose executable does not exist. The failure shows up a moment later in the journal.
Type=exec fixes that. It behaves like simple, but systemctl start waits until the binary has been executed. A wrong path or a permission problem fails the start command itself. For a normal foreground process, this is the type I use:
[Service]
Type=exec
ExecStart=/usr/bin/node /opt/demo-api/server.js
Type=notify needs help from the application. The program calls sd_notify() and sends READY=1 when it can really serve traffic. Then anything ordered After= this service waits for that signal, not for a process to appear. Only use it when the application documents that support. If it never sends the message, startup hangs.
Type=forking exists for traditional daemons that detach: the parent exits, a child keeps running. Pair it with PIDFile= so systemd can track the right process. New software should stay in the foreground and skip this type entirely.
Type=oneshot runs finite setup work. systemd waits for the process to exit before it considers the unit started. Add RemainAfterExit=yes when the unit should stay active (exited) after finishing:
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/prepare-cache-dirs
Without RemainAfterExit=, the unit goes back to inactive as soon as the script ends. With it, other units can depend on the fact that the setup happened.
The failure mode to recognize
The classic mismatch is Type=forking on a program that never forks. systemd waits for the parent to exit. The parent never does. Startup hangs until the timeout kills it:
demo-api.service: start operation timed out. Terminating.
demo-api.service: Failed with result 'timeout'.
The process was healthy the whole time. The unit just described it wrongly. The fix is not a longer timeout. It is the right type.
Before writing a unit, look at how the application starts. Does it stay in the foreground? Does it detach? Does it finish and exit? Write down, in one sentence, the event that should count as “started”. The type follows from that sentence. Try this with one program you run today: for a Node.js server the answer is exec, for a migration script it is oneshot.
Lesson completed