SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Is init.d deprecated? What systemd 260 removed

systemd 260 removed the bridge that started init.d scripts and rc.local at boot. Audit your 26.04 server and convert each script to a native unit first.

Is init.d deprecated?

Yes. Init.d scripts are deprecated in systemd, and since systemd 260 systemd no longer starts them for you. The scripts are not forbidden. An executable file in /etc/init.d still runs when you call it by hand. On 17 March 2026, systemd 260 removed the bridge that turned those scripts, and /etc/rc.local, into units at boot. That bridge is gone on any server that upgrades to a release built on systemd 260 or newer. A daemon that has only an init.d script then has nothing to start it at boot. The same goes for commands that live only in rc.local.

Ubuntu 26.04 LTS still has the bridge. Ubuntu 26.10 and Debian's testing suite do not. This guide shows how to find what depends on the bridge on a 26.04 or Debian 13 server. It then shows how to replace each piece with a native systemd unit before you upgrade.

What did systemd 260 remove?

The systemd 260 release notes say it in one line: "Support for System V service scripts has been removed." They then list the components that went with it:

  • systemd-sysv-generator, which wrote a wrapper .service unit for each script in /etc/init.d.
  • systemd-sysv-install, the hook that let systemctl enable, disable and is-enabled act on init.d scripts.
  • systemd-rc-local-generator, which hooked rc-local.service into boot when /etc/rc.local was executable.
  • rc-local.service itself, the unit that ran /etc/rc.local start.

A generator is a small program that systemd runs before it loads units. It writes unit files for systemd to read. The SysV generator read each script's LSB (Linux Standard Base) header, which is the ### BEGIN INIT INFO comment block, and turned it into a unit. Without the generator, systemd never looks inside /etc/init.d. A script there then has no unit of its own name.

None of this was a surprise. The systemd 259 man page for the SysV generator already said "this component is deprecated and scheduled for removal". The rc-local man page went further: "it is strongly recommended to avoid using this script today." The 260 release notes also mark the matching build options (-Drc-local=, -Dsysvinit-path= and -Dsysvrcnd-path=) as deprecated, to be dropped in a later release.

For the longer story of how SysV init gave way to units, read how systemd replaced SysV init on Linux.

Which releases still ship the SysV generator?

Key this on the systemd version, not the release name. The version decides whether the bridge exists. The rows below come from packages.ubuntu.com and packages.debian.org, read in October 2026.

Chartsystemd version and SysV bridge by release, October 2026
The data behind this chart
[
  {
    "distro": "Ubuntu 24.04 LTS",
    "systemd": 255,
    "exact_package": "255.4-1ubuntu8.17",
    "sysv_and_rc_local": "present (version is older than 260)"
  },
  {
    "distro": "Debian 13 trixie",
    "systemd": 257,
    "exact_package": "257.13-1~deb13u1",
    "sysv_and_rc_local": "present (in package file list)"
  },
  {
    "distro": "Ubuntu 26.04 LTS",
    "systemd": 259,
    "exact_package": "259.5-0ubuntu3.4",
    "sysv_and_rc_local": "present (in package file list)"
  },
  {
    "distro": "Ubuntu 26.10 (development)",
    "systemd": 261,
    "exact_package": "261.3-0ubuntu3",
    "sysv_and_rc_local": "absent (not in package file list)"
  },
  {
    "distro": "Debian forky and sid",
    "systemd": 262,
    "exact_package": "262-1",
    "sysv_and_rc_local": "absent (not in package file list)"
  }
]

Ubuntu 26.04 LTS ships systemd 259.5-0ubuntu3.4. Its amd64 file list still holds all four pieces, plus a drop-in file for rc-local.service. Debian 13 ships 257.13-1~deb13u1 and has the same four.

Ubuntu 26.10 is not released yet. Its final release is scheduled for 15 October 2026. Its development archive, the suite named stonking, carries systemd 261.3-0ubuntu3. Its file list has no file with sysv or rc-local in its path. Debian forky (the current testing suite) and sid carry 262-1, and their file list has neither file either. Open those pages and search for sysv to see this for yourself.

No Ubuntu release carries systemd 260. The archive goes straight from 259 to 261. The Ubuntu 24.04 row is marked from the version alone: 255 is older than the removal, so the pieces are still in place.

The table leaves out Rocky Linux, AlmaLinux, RHEL and Fedora because their package versions were not checked for this guide. The rule is the same for them: read the systemd version on the box.

If you run LTS releases only, the difference between LTS and interim Ubuntu releases on a server decides when you meet this change. For the other 26.10 changes, see what changes on a server in Ubuntu 26.10.

Check whether your server still has the generators

Run two commands and read the result yourself:

systemctl --version
ls /usr/lib/systemd/system-generators/

Read the version number from the first command. Then look for two names in the listing: systemd-sysv-generator and systemd-rc-local-generator. On systemd 259 and older they are present, so init.d scripts and rc.local still start at boot. On systemd 260 and newer they are gone. If you see them, you still have time to convert.

Audit /etc/init.d before you upgrade

Start with the plain list:

ls -l /etc/init.d/

Many entries there are harmless, because a package can ship an init.d script next to a native unit of the same name. The generator skips a script when a native unit with that name already exists. It also skips scripts that are not executable. So the scripts that matter are the executable ones with no native unit beside them. This loop finds them:

for f in /etc/init.d/*; do
  [ -f "$f" ] && [ -x "$f" ] || continue
  name=${f##*/}
  native=no
  for d in /etc/systemd/system /run/systemd/system /usr/local/lib/systemd/system /usr/lib/systemd/system; do
    if [ -e "$d/$name.service" ] || [ -L "$d/$name.service" ]; then
      native=yes
    fi
  done
  if [ "$native" = no ]; then
    echo "init.d only: $name"
  fi
done

Each line it prints is a script that only the generator is starting today. No output means every executable script already has a native unit. The -L test matters: a unit masked as a symlink to /dev/null still counts as a native unit.

For each name it prints, answer three questions on the live system. Take ticker as the example name.

ls /etc/rc?.d/ | grep ticker
systemctl show -p SourcePath --value ticker.service
systemctl cat ticker.service

The first command shows the runlevel links. The generator ignores the Default-Start: header and reads these links instead. An S link in rc2.d, rc3.d or rc4.d hooks the script into multi-user.target. An S link in rc5.d hooks it into graphical.target. A script with no S links was never started at boot, even on 26.04.

The second command tells you where the unit came from. The generator writes a SourcePath= line into every wrapper it makes. If that value is the script's path under /etc/init.d, the unit is generated, and it will disappear after the upgrade. systemctl cat prints the file behind the unit, with the file name in a comment. A generated unit also names the generator near the top.

Finally, find out who owns the script:

dpkg -S /etc/init.d/ticker

If a package owns it, the real fix belongs in that package. Check for a newer version, or report it to the maintainer. Until then, write your own unit under /etc/systemd/system with the same name, as shown below. A unit there takes priority over the package's files.

Does /etc/rc.local still run on Ubuntu 26.04?

Yes, but only when the file exists and is executable. The rc-local generator checks that the file is executable before it pulls rc-local.service into multi-user.target. The unit itself also carries ConditionFileIsExecutable=. Check both things:

ls -l /etc/rc.local
grep -vE '^[[:space:]]*(#|$)' /etc/rc.local | grep -vx 'exit 0'

If ls reports no such file, there is nothing to migrate. If the mode has no x, the file has never run on this box, whatever it contains. If the grep prints lines, those are real commands. Each one needs a new home before you move to systemd 260 or newer.

Convert an init.d script to a native systemd unit

This section builds a toy daemon with an old-style init script, then converts it. The daemon is a shell loop, so it is safe to follow on a test server.

Create the toy daemon and its init script

The daemon prints a timestamp every 10 seconds. With --daemon it forks into the background and writes a PID file, which is what an old-style daemon does.

sudo tee /usr/local/bin/ticker > /dev/null <<'EOF'
#!/bin/sh
# Toy daemon: print a timestamp every 10 seconds.
# With --daemon it forks into the background and writes a PID file.
tick() {
  while true; do
    date '+ticker: %F %T'
    sleep 10
  done
}

if [ "$1" = "--daemon" ]; then
  tick >> /var/log/ticker.log 2>&1 &
  echo $! > /run/ticker.pid
  exit 0
fi

tick
EOF
sudo chmod 755 /usr/local/bin/ticker

Now the init script, with a typical LSB header:

sudo tee /etc/init.d/ticker > /dev/null <<'EOF'
#!/bin/sh
### BEGIN INIT INFO
# Provides:          ticker
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Should-Start:      $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Toy ticker daemon
### END INIT INFO

case "$1" in
  start)
    /usr/local/bin/ticker --daemon
    ;;
  stop)
    [ -f /run/ticker.pid ] && kill "$(cat /run/ticker.pid)" && rm -f /run/ticker.pid
    ;;
  *)
    echo "Usage: $0 start or stop"
    exit 1
    ;;
esac
EOF
sudo chmod 755 /etc/init.d/ticker

Run the audit loop again. It should now print init.d only: ticker.

Map the LSB header to unit directives

Each header line has a direct equivalent:

  • Provides: ticker becomes the file name, ticker.service.
  • Short-Description: becomes Description=.
  • Required-Start: $remote_fs becomes After=remote-fs.target. The generator turned both Required-Start: and Should-Start: into After= lines and never wrote Requires=, so After= keeps the old behaviour.
  • $syslog needs no line. The generator dropped it too, and systemd sends a service's output to the journal by default.
  • Should-Start: $network becomes After=network-online.target plus Wants=network-online.target. The generator did the same, because that target has to be pulled in actively. Wants= alone sets no start order, and After= alone pulls nothing in, so you need both lines.
  • Default-Start: 2 3 4 5 becomes WantedBy=multi-user.target. This is a rule of thumb, not something systemd reads from the header. Runlevels 2 to 4 map to multi-user.target, and a server rarely needs anything that only runlevel 5 adds.

Required-Stop: and Default-Stop: need no line. systemd stops units in the reverse of their start order.

The literal translation: Type=forking with PIDFile=

This unit keeps the daemon's old behaviour. The main process forks and the parent exits. systemd then reads the PID file to learn which process to watch.

[Unit]
Description=Toy ticker daemon
After=remote-fs.target network-online.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/ticker.pid
ExecStart=/usr/local/bin/ticker --daemon

[Install]
WantedBy=multi-user.target

It works, but the systemd docs argue against it. The service man page says of Type=forking: "The use of this type is discouraged". Of PIDFile= it says: "PID files should be avoided in modern projects." Both exist for old daemons you cannot change.

The better unit: run it in the foreground with Type=exec

If the daemon can stay in the foreground, let it. With Type=exec, systemd treats the unit as started once the main binary has been executed. systemd knows the main process from the first moment, so there is no PID file to read and none to go out of date. The output goes to the journal instead of a log file you rotate yourself. Restart=on-failure also becomes useful. The wrapper the generator wrote used Restart=no, so a crashed init.d daemon was never restarted.

sudo tee /etc/systemd/system/ticker.service > /dev/null <<'EOF'
[Unit]
Description=Toy ticker daemon
After=remote-fs.target network-online.target
Wants=network-online.target

[Service]
Type=exec
ExecStart=/usr/local/bin/ticker
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

A daemon that supports systemd's readiness messages is better still as Type=notify. A shell loop does not, so Type=exec is the right choice here. For the full trade-off, read how systemd service types decide when a unit has started.

Verify, enable and retire the script

Check the unit before you load it:

sudo systemd-analyze verify /etc/systemd/system/ticker.service

This command loads the unit and prints warnings for errors. It reports unknown sections and directives. It also reports an ExecStart= command that is missing or not executable, which is why the toy daemon was created and made executable first. A warning that names ticker.service is a problem to fix before you go on.

Then load and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now ticker.service
systemctl status ticker.service
journalctl -u ticker.service -n 5

systemctl status should show active (running), and the journal should show a ticker: line every 10 seconds. If the unit fails instead, the exit codes of a systemd unit that will not start explain what each status code means.

The native unit now has the same name as the script, so the generator on 26.04 skips the script. Remove the script and the old files so nothing confuses the next person:

sudo rm /etc/init.d/ticker
sudo rm -f /var/log/ticker.log /run/ticker.pid

Run the audit loop a third time. ticker should no longer appear.

Replace /etc/rc.local with a oneshot unit

A oneshot unit runs commands that finish, then stops. With RemainAfterExit=yes, systemd keeps the unit marked active after the commands exit, so systemctl status shows that it ran.

Move the commands into a script first:

sudo tee /usr/local/sbin/boot-local.sh > /dev/null <<'EOF'
#!/bin/sh
# Commands moved from /etc/rc.local go here.
logger -t boot-local "boot-local.sh ran"
EOF
sudo chmod 755 /usr/local/sbin/boot-local.sh

Then the unit:

sudo tee /etc/systemd/system/boot-local.service > /dev/null <<'EOF'
[Unit]
Description=Local boot commands, formerly /etc/rc.local
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/boot-local.sh

[Install]
WantedBy=multi-user.target
EOF
sudo systemd-analyze verify /etc/systemd/system/boot-local.service

The network lines are a real change. The upstream rc-local.service is ordered after network.target, not network-online.target. The systemd docs say that units which strictly need a configured network should pull in network-online.target and order themselves after it. So an rc.local command that used the network could run before the network was configured. This unit waits for it.

Enable it, then stop rc.local from running the same commands a second time:

sudo systemctl daemon-reload
sudo systemctl enable --now boot-local.service
sudo chmod -x /etc/rc.local
journalctl -t boot-local -n 1

The chmod -x is enough on 26.04, because the generator only pulls in rc-local.service when the file is executable. The journal line proves the new unit ran.

If rc.local started a long-running process with &, do not put it in the oneshot script. Give it its own Type=exec unit, as in the ticker example above.

What to check after the upgrade

Run the audit loop and the rc.local check on the old release. Fix every line they print, and only then upgrade. The upgrade steps themselves are in upgrading Ubuntu 24.04 to 26.04, and the same habits apply to the next one. After the first boot on systemd 260 or newer, run the generator listing again to confirm the two names are gone. Then look for anything that did not come up:

systemctl --failed

A converted unit that failed will show up there. A daemon that only had an init.d script will not, because systemd has no unit of that name to fail. That is why the audit has to happen before the upgrade. If you track Debian testing, the difference between Debian stable, testing and unstable explains why forky already has this change and Debian 13 does not.

FAQ

Is init.d deprecated in systemd?

Yes. systemd 260, released on 17 March 2026, removed support for System V service scripts. It removed systemd-sysv-generator, systemd-sysv-install, systemd-rc-local-generator and rc-local.service. An executable file in /etc/init.d still runs when you call it by hand. But systemd no longer turns it into a unit, so it is not started at boot.

Will my /etc/init.d script still start after upgrading to Ubuntu 26.10?

Not unless it has a native unit. The Ubuntu 26.10 development archive carries systemd 261.3-0ubuntu3, and its package file list has no SysV or rc-local generator. Before you upgrade, list the executable scripts in /etc/init.d that have no .service file of the same name. Then write a native unit for each one.

Does /etc/rc.local still work on Ubuntu 26.04?

Yes. Ubuntu 26.04 LTS ships systemd 259.5-0ubuntu3.4, which still includes the rc-local generator and rc-local.service. The file only runs if it exists and is executable. On systemd 260 and newer it is ignored. Move its commands into a Type=oneshot unit with RemainAfterExit=yes, and order that unit after network-online.target with Wants= and After=.

How do I convert an init.d script to a systemd service?

Read the LSB header. Turn Required-Start: and Should-Start: into After= lines, and add Wants=network-online.target when the script needs $network. Use WantedBy=multi-user.target for a script started in runlevels 2 to 5. If the daemon can run in the foreground, use Type=exec. Otherwise use Type=forking with PIDFile=. Check the file with systemd-analyze verify, then enable it.

How do I know which systemd version my server runs?

Run systemctl --version and read the version number. Then run ls /usr/lib/systemd/system-generators/. If systemd-sysv-generator and systemd-rc-local-generator are listed, the box is on systemd 259 or older. It still starts init.d scripts and rc.local at boot.