init.d scripts are a relic of a bygone era, and bringing them into the modern systemd world feels like trying to fit a square peg into a round hole. The core issue isn’t just about starting and stopping services; it’s about how systemd manages dependencies, logging, and resource allocation in a fundamentally different way than init.d’s sequential, event-driven model.
Let’s see this in action. Imagine a simple init.d script for a hypothetical my-app service.
#!/bin/bash
# /etc/init.d/my-app
DAEMON=/usr/sbin/my-app
PIDFILE=/var/run/my-app.pid
NAME=my-app
test -x $DAEMON || exit 0
case "$1" in
start)
echo -n "Starting $NAME: "
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --background --make-pidfile
echo "$NAME."
;;
stop)
echo -n "Stopping $NAME: "
start-stop-daemon --stop --quiet --pidfile $PIDFILE --exec $DAEMON
echo "$NAME."
;;
restart)
echo -n "Restarting $NAME: "
start-stop-daemon --stop --quiet --pidfile $PIDFILE --exec $DAEMON
sleep 1
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --background --make-pidfile
echo "$NAME."
;;
status)
if [ -f $PIDFILE ]; then
PID=$(cat $PIDFILE)
if ps -p $PID > /dev/null; then
echo "$NAME is running (pid $PID)."
exit 0
else
echo "$NAME is not running (stale pid file $PIDFILE)."
exit 1
fi
else
echo "$NAME is not running."
exit 3
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
To convert this, we’ll create a systemd unit file, typically placed in /etc/systemd/system/my-app.service. This file tells systemd how to manage my-app.
[Unit]
Description=My Application Service
After=network.target
[Service]
Type=simple
User=myuser
Group=mygroup
WorkingDirectory=/opt/my-app
ExecStart=/usr/sbin/my-app --config /etc/my-app/config.yml
ExecStop=/bin/kill -s TERM $MAINPID
Restart=on-failure
PIDFile=/var/run/my-app.pid
[Install]
WantedBy=multi-user.target
The [Unit] section is systemd’s way of defining metadata and dependencies. Description is a human-readable name. After=network.target is crucial; it tells systemd that my-app should only start after the network stack is up, a concept init.d handled implicitly or through very basic LSB tags.
The [Service] section is where the magic happens. Type=simple is the most common type, assuming the ExecStart command is the main process and systemd monitors it. If your init.d script used daemon or start-stop-daemon --background, systemd can manage that directly. User and Group are direct mappings for running the process with specific privileges, avoiding the need for sudo within the script or running as root unnecessarily. WorkingDirectory sets the context for the application. ExecStart is the command to run, and unlike init.d which often relies on the script itself to fork, systemd handles the process management. ExecStop defines how to stop the service; here, we send a TERM signal to the main process ID ($MAINPID) that systemd tracks. Restart=on-failure is a powerful feature, automatically restarting the service if it crashes, something that required custom logic in init.d. PIDFile tells systemd where the process ID is stored, allowing it to manage the process lifecycle.
The [Install] section determines how the service is enabled. WantedBy=multi-user.target means this service should be started when the system reaches the standard multi-user runlevel.
After creating my-app.service, you’d run sudo systemctl daemon-reload to inform systemd of the new file, then sudo systemctl enable my-app.service to make it start on boot, and sudo systemctl start my-app.service to start it immediately.
The true power of systemd comes from its ability to manage services as a dependency graph rather than a linear sequence. For instance, if my-app depends on a database service, you’d add Requires=database.service and After=database.service in the [Unit] section. systemd will then ensure the database is running and healthy before attempting to start my-app, and if the database stops, systemd can be configured to stop my-app too. This declarative approach dramatically simplifies complex startup sequences and makes the system more robust.
A subtle but important detail often missed is how systemd handles standard output and error. Instead of redirecting to log files within the init.d script, systemd automatically captures stdout and stderr from ExecStart and makes them available via journalctl. You can then view logs for your service with journalctl -u my-app.service. This centralizes logging and makes it much easier to debug.
The most counterintuitive aspect of migrating from init.d is realizing that you often don’t need to replicate the exact logic of the init.d script. systemd provides built-in mechanisms for many common tasks like daemonizing, PID file management, and even basic signal handling. The goal is to describe the desired state of your service (e.g., "this executable should be running with these arguments, under this user, and if it crashes, restart it") rather than detailing the steps to achieve that state, which is what init.d scripting often devolved into.
Once your my-app.service is running, you’ll likely encounter issues with how systemd manages resource limits.