Converting SysV Init Scripts to systemd Service Units
For more than a decade, systemd quietly ran old System V init scripts, the shell scripts in /etc/init.d/, through a compatibility layer. That layer has now been removed from recent systemd releases; Ubuntu 26.10, with systemd 261, no longer runs them. Any service you still start from an init script needs a proper unit file.
Most packaged software switched long ago. The scripts that remain are usually local: an in-house application, an old vendor package, a script someone wrote years ago. Here is how to convert them.
Find what still depends on init scripts
Before upgrading, while the compatibility layer still works:
ls /etc/init.d/
systemctl list-units --all --type=service | grep -iE 'lsb|sysv'
For each script, check whether a native unit already exists:
systemctl cat myapp.service 2>&1 | head -3
If the first line points to a file under /run/systemd/generator.late/, that unit was generated from the init script and will disappear.
Anatomy of an init script
A typical SysV script looks like this:
#!/bin/sh
### BEGIN INIT INFO
# Provides: myapp
# Required-Start: $network $remote_fs
# Required-Stop: $network $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: My application server
### END INIT INFO
DAEMON=/opt/myapp/bin/myapp
PIDFILE=/var/run/myapp.pid
USER=myapp
case "$1" in
start)
start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE \
--chuid $USER --exec $DAEMON -- --config /etc/myapp.conf
;;
stop)
start-stop-daemon --stop --pidfile $PIDFILE
;;
restart) $0 stop; $0 start ;;
esac
Most of it is boilerplate that systemd handles for you: backgrounding, PID files, user switching, and dependency ordering.
The translation
| Init script | systemd unit |
|---|---|
Description: | Description= in [Unit] |
Required-Start: $network | After=network-online.target and Wants=network-online.target |
Required-Start: $remote_fs | After=remote-fs.target |
Default-Start: 2 3 4 5 | WantedBy=multi-user.target in [Install] |
start command | ExecStart= |
stop command | Usually nothing: systemd sends SIGTERM |
--chuid myapp | User=myapp |
--background, PID files | Not needed if the program runs in the foreground |
reload command | ExecReload= |
The converted unit:
# /etc/systemd/system/myapp.service
[Unit]
Description=My application server
Wants=network-online.target
After=network-online.target remote-fs.target
[Service]
Type=exec
User=myapp
Group=myapp
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp.conf
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Note what disappeared: no PID file, no backgrounding, no stop logic. systemd keeps the process in a cgroup, knows exactly which processes belong to it, and stops all of them cleanly. And Restart=on-failure gives you something the init script never had.
Our systemd unit builder generates units like this from a form.
Daemons that insist on forking
Some programs always detach into the background. Prefer a flag that keeps them in the foreground, often -f, --foreground, --nodaemon, or -D; check the man page. If there is none:
[Service]
Type=forking
PIDFile=/run/myapp/myapp.pid
RuntimeDirectory=myapp
ExecStart=/opt/myapp/bin/myapp --daemon
RuntimeDirectory= creates /run/myapp owned by the service user, a common need for PID files.
Scripts that do more than start a program
Init scripts sometimes create directories, set limits, or run setup before starting. systemd has directives for most of that:
| Script does | Use instead |
|---|---|
mkdir /var/run/myapp | RuntimeDirectory=myapp |
mkdir /var/lib/myapp | StateDirectory=myapp |
ulimit -n 65536 | LimitNOFILE=65536 |
export VAR=value | Environment=VAR=value or EnvironmentFile= |
cd /opt/myapp | WorkingDirectory=/opt/myapp |
| A setup command before start | ExecStartPre= |
Test and enable
sudo systemd-analyze verify /etc/systemd/system/myapp.service
sudo systemctl daemon-reload
sudo systemctl start myapp
systemctl status myapp
journalctl -u myapp -f
sudo systemctl enable myapp
Once the unit works, remove or rename the old script so nothing calls it by accident:
sudo mv /etc/init.d/myapp /root/myapp.initd.bak
Reboot once to confirm it starts on its own.
Go further
A converted unit is a good moment to add hardening that init scripts could never offer: NoNewPrivileges=yes, ProtectSystem=strict, PrivateTmp=yes, and more. Our systemd service hardening guide explains each, and writing systemd service files covers the rest of the unit format.
Frequently Asked Questions
Does systemd still run /etc/init.d scripts?
For many years systemd ran them through a compatibility generator that turned each script into a temporary unit. Recent systemd releases have removed that support; Ubuntu 26.10, for example, ships a systemd that no longer runs legacy System V scripts. Services that relied on them need a real unit file.
How do I find services that still use init scripts?
List files in /etc/init.d and check which have no matching unit with systemctl cat. Before upgrading, systemctl list-units —all can show units with descriptions mentioning LSB or SYSV, which were generated from scripts.
What Type should I use for a converted service?
If the program can run in the foreground, use Type=simple or Type=exec and start it without its daemonise option. If it can only fork into the background, use Type=forking and set PIDFile= to the file it writes so systemd can track the main process.
Where do runlevels go?
Runlevels become targets. Runlevel 3 corresponds to multi-user.target and runlevel 5 to graphical.target. A service that started in runlevels 2 to 5 normally gets WantedBy=multi-user.target in its Install section.
What replaces the start-stop-daemon user switching?
The User= and Group= directives in the Service section. systemd starts the process as that user directly, and you can add hardening options such as NoNewPrivileges=yes and ProtectSystem=strict.
How do I test the new unit?
Run systemd-analyze verify on the file to catch errors, then systemctl daemon-reload, systemctl start, and systemctl status. Check journalctl -u for output, and reboot once to confirm it starts at boot.