An old /etc/rc.local script inherited from a migrated server still runs under systemd, and rc-local.service is what makes it happen. This guide creates a small executable script, makes systemd discover it at boot, and verifies the resulting service. The installed systemd here is version 255.4-1ubuntu8.17. Allow about fifteen minutes, including a reboot only if you need to test the complete boot path.
This is a compatibility procedure for an existing legacy script. For new boot tasks, a dedicated systemd unit is usually easier to order, monitor and remove. You need root access, a shell, and a clear understanding of every command the script will run. Editing a boot script is a privileged and service-disrupting change, so keep a recovery shell or console available before you begin.
The generator does not enable a permanently installed service in the usual sense. During system startup, it checks whether /etc/rc.local exists and is executable. Only then does it pull rc-local.service into the boot transaction. The path is compiled into systemd and can differ on another distribution.
Run these read-only checks as an ordinary user first:
$ test -e /etc/rc.local && echo exists || echo absent
absent
$ systemctl is-enabled rc-local.service
static
static is not an error here. The unit is conditional and is intended to be pulled in by the generator when its script exists and has execute permission. On this machine, /etc/rc.local is absent, so the compatibility service is inactive.
Checkpoint: Do not create the file until you have identified the exact commands that belong in it. A typo in a boot script can delay or break startup.
Use an editor as root to create the file. The following example records a boot marker in a dedicated log, then exits successfully. The marker is useful because the service itself normally has no interactive terminal.
$ sudo install -o root -g root -m 0755 /dev/null /etc/rc.local
$ sudoedit /etc/rc.local
Put this content in the editor:
#!/bin/sh
printf '%s\n' "rc.local reached at $(date -Is)" >> /var/log/rc.local-test.log
exit 0
The shebang is required because systemd executes the file directly. The file must remain executable, and the commands must return promptly. A command that waits forever can leave the service running indefinitely because the packaged unit has an infinite start timeout.
Check the saved file and its mode:
$ sudo head -n 5 /etc/rc.local
#!/bin/sh
printf '%s\n' "rc.local reached at $(date -Is)" >> /var/log/rc.local-test.log
exit 0
$ stat -c '%A %a %U:%G %n' /etc/rc.local
-rwxr-xr-x 755 root:root /etc/rc.local
Your timestamp will differ. The important checks are the interpreter line, root ownership, and execute bits for the file.
Creating the file changes the generator's input. Ask the system manager to reload its configuration as root:
$ sudo systemctl daemon-reload
$ systemctl status rc-local.service --no-pager
○ rc-local.service - /etc/rc.local Compatibility
Loaded: loaded (/usr/lib/systemd/system/rc-local.service; static)
Active: inactive (dead)
The exact status layout varies. Seeing the unit loaded but inactive is expected until you start it or reboot. daemon-reload reruns generators and rebuilds the dependency tree; it does not run the script by itself.
Inspect the unit before starting it:
$ systemctl cat rc-local.service
[Unit]
Description=/etc/rc.local Compatibility
ConditionFileIsExecutable=/etc/rc.local
After=network.target
[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=infinity
RemainAfterExit=yes
Distribution drop-ins may add settings. On this Ubuntu installation, a drop-in also orders the unit after network-online.target and sends output to the journal and console. Inspect the complete output on your own host rather than assuming another distribution has the same additions.
Starting the service now is a privileged, real execution of every command in /etc/rc.local. Review the file first, then run:
$ sudo systemctl start rc-local.service
$ systemctl is-active rc-local.service
active
$ sudo tail -n 1 /var/log/rc.local-test.log
rc.local reached at 2026-09-27T07:30:00+01:00
The timestamp is only an example. The useful result is an active service and one new log line. Because the unit uses RemainAfterExit=yes, an active state means the start operation completed and the unit is being kept active. It does not mean a long-running process from the script is being supervised.
If the start fails, read the service log before changing anything:
$ sudo journalctl -u rc-local.service -b --no-pager
$ systemctl status rc-local.service --no-pager
Common causes are a missing execute bit, a bad shebang, a command that is not on the boot-time PATH, or a script that assumes a terminal. Use absolute paths and explicit environment setup where needed.
The manual describes the compatibility service as late boot, after network.target, but in parallel with most regular services. That is not the same as "last". Another service can still be starting, and network.target does not prove that an address, route or remote service is ready.
If the script genuinely needs a configured network connection, add a drop-in rather than editing the vendor unit:
$ sudo install -d -m 0755 /etc/systemd/system/rc-local.service.d
$ sudoedit /etc/systemd/system/rc-local.service.d/network.conf
[Unit]
Wants=network-online.target
After=network-online.target
Then reload and restart the service during a maintenance window:
$ sudo systemctl daemon-reload
$ sudo systemctl restart rc-local.service
$ systemctl show -p After,Wants rc-local.service
network-online.target is still only as useful as the network manager's wait-online implementation. If the task has more precise dependencies, a dedicated unit can express them more clearly.
Do not turn the compatibility script into an unreviewed collection of unrelated boot commands. Replace the harmless marker with the smallest tested action, use absolute paths, log failures, and make repeated runs safe. The service runs with root privileges, so a command injection bug or an unsafe writable input can become a full system compromise.
Before changing the test, keep a recoverable copy:
$ sudo cp --preserve=all /etc/rc.local /etc/rc.local.working
$ sudoedit /etc/rc.local
If the new version causes trouble, restore the copy and reload the manager:
$ sudo cp --preserve=all /etc/rc.local.working /etc/rc.local
$ sudo systemctl daemon-reload
$ sudo systemctl restart rc-local.service
Once you no longer need the compatibility path, remove the file and any drop-in you created, then reload systemd. Do this only after moving the work to a tested replacement unit or confirming that it is no longer required:
$ sudo rm /etc/systemd/system/rc-local.service.d/network.conf
$ sudo rmdir /etc/systemd/system/rc-local.service.d
$ sudo rm /etc/rc.local
$ sudo systemctl daemon-reload
Those removals are destructive. Keep the backup until the replacement has survived a complete boot.
/etc/rc.local exists, starts with a valid shebang, and is executable by root.rc-local.service is loaded after daemon-reload.network.target with a working network connection or treated rc.local as the final boot step.