Introduction
You have a script โ a backup job, a fan speed tweak, a tunnel, a container restart โ and you need it to run every time the machine boots. Ubuntu offers multiple ways to hook into the boot sequence, each with different trade-offs: systemd service files, cron’s @reboot macro, legacy rc.local, SysV init scripts, and the desktop’s Startup Applications menu.
This guide covers all five methods, focusing on systemd as the modern, recommended approach. You will learn how to set up each one, when to pick which, and how to debug them when something goes wrong.
Script Requirements
Before any method works, the script itself must meet a few conditions.
Shebang Line
The first line must be a valid shebang so the kernel knows which interpreter to use:
#!/bin/bash
Other common shebangs:
#!/usr/bin/env python3
#!/bin/bash -e
#!/usr/bin/env bash
Executable Permission
The file must have the execute bit set:
chmod +x /path/to/your-script.sh
If you forget this step, every method below will fail silently or with a permission-denied error.
Absolute Paths
Startup scripts run with a minimal environment โ PATH is often /usr/bin:/bin and nothing else. Never rely on relative paths or ~. Use full paths for everything:
#!/bin/bash
LOG_FILE="/var/log/my-startup-script.log"
echo "Started at $(date)" >> "$LOG_FILE"
/usr/bin/rsync -a /data/ /backup/
Idempotency
The script may run on every boot, which can happen unexpectedly after a crash or power loss. Design it to be safe to run multiple times โ overwrite temporary files, check for lock files, or skip work already done.
#!/bin/bash
LOCKFILE="/var/run/my-startup-script.lock"
if [ -f "$LOCKFILE" ]; then
exit 0
fi
touch "$LOCKFILE"
# do work
Method 1: Systemd Service (Recommended)
Systemd is the default init system on Ubuntu (15.04+). Service files give you fine-grained control over dependencies, ordering, restart behaviour, environment, and logging.
Basic Oneshot Service
A oneshot service runs a script once and exits. This is the right type for most startup tasks.
Create /etc/systemd/system/set-fan-speed.service:
[Unit]
Description=Set fan speed on boot
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/set-fan-speed.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Enable and test:
sudo systemctl daemon-reload
sudo systemctl enable set-fan-speed.service
sudo systemctl start set-fan-speed.service
sudo systemctl status set-fan-speed.service
RemainAfterExit=yes keeps the service in an active state after the script finishes, which prevents systemd from restarting it and makes dependency chains cleaner.
Simple Service (Long-Running Process)
For a script that stays running โ a monitoring agent, a reverse tunnel, a file watcher โ use Type=simple:
[Unit]
Description=Reverse SSH tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/start-tunnel.sh
Restart=on-failure
RestartSec=10
User=tunnel
[Install]
WantedBy=multi-user.target
Restart=on-failure together with RestartSec=10 means systemd will restart the script if it crashes, waiting ten seconds between attempts.
User Service (No Sudo)
If the script only needs the current user’s privileges, place the service file in the user directory:
mkdir -p ~/.config/systemd/user
Create ~/.config/systemd/user/notify-me.service:
[Unit]
Description=Startup notification
[Service]
Type=oneshot
ExecStart=/home/user/bin/notify.sh
[Install]
WantedBy=default.target
Enable it with the --user flag:
systemctl --user daemon-reload
systemctl --user enable notify-me.service
systemctl --user start notify-me.service
User services start when the user logs in, not at system boot. This is useful for GUI tools, desktop notifications, or personal daemons.
Handling Dependencies
Use After= and Requires= to order your service relative to other units:
[Unit]
Description=Mount NFS share
After=network-online.target
Wants=network-online.target
After=remote-fs.target
Wants=starts the target if possible but does not fail if it cannot.Requires=fails the whole unit if the dependency fails.After=controls ordering only, not activation.
Environment Variables
Systemd strips most environment variables. Set them explicitly in the service file:
[Service]
Type=oneshot
Environment="MY_VAR=hello"
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/start.sh
Or load them from a file:
[Service]
EnvironmentFile=/etc/default/myapp
ExecStart=/opt/myapp/start.sh
Capturing Output
Systemd captures stdout and stderr from the script automatically. View it with journalctl:
sudo journalctl -u set-fan-speed.service
To follow the log in real time:
sudo journalctl -u set-fan-speed.service -f
If you also want the output written to a plain file, redirect inside the script:
#!/bin/bash
exec &>> /var/log/my-startup.log
echo "Script started at $(date)"
Method 2: Cron @reboot
The cron daemon supports the @reboot macro, which runs a command once when the system boots. This approach is simple and requires no service files.
Open the root crontab:
sudo crontab -e
Add a line:
@reboot /usr/local/bin/backup-data.sh
For a user-specific job (runs at user login, not system boot):
crontab -e
@reboot /home/user/bin/personal-script.sh
Key behaviour:
@rebootruns after cron starts, which is early in the boot sequence โ before network mounts, before most services.- Environment is minimal. Wrap your script in a helper that sets
PATHand other variables. - Logging is non-existent by default. Redirect output manually:
@reboot /usr/local/bin/backup-data.sh >> /var/log/backup-cron.log 2>&1
Pros: dead simple, no systemd knowledge needed. Cons: no dependency control, no restart-on-failure, no status checking, runs as a one-shot only.
Method 3: rc.local (Legacy)
Before systemd, Ubuntu used /etc/rc.local โ a script that runs at the end of the boot sequence. It still works on most Ubuntu releases if you enable it.
Create or edit /etc/rc.local:
#!/bin/sh -e
#
# rc.local
#
/usr/local/bin/set-fan-speed.sh
exit 0
Make it executable:
sudo chmod +x /etc/rc.local
Enable the rc-local service:
sudo systemctl enable rc-local
sudo systemctl start rc-local
sudo systemctl status rc-local
rc.local runs as root, after most services, with a minimal environment. It is a single script โ if one command fails, the whole file stops (because of sh -e). Use || true for commands that may fail:
/usr/local/bin/set-fan-speed.sh || true
/usr/local/bin/backup-data.sh || true
This method is deprecated on newer Ubuntu releases (20.04+) and may be removed. Prefer systemd for new setups.
Method 4: Init.d Scripts (SysV Legacy)
Very old Ubuntu installations used SysV init scripts in /etc/init.d/. These are still supported through systemd’s compatibility layer but are not recommended for new work.
A minimal init.d script:
#!/bin/bash
### BEGIN INIT INFO
# Provides: my-startup
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Run script on startup
### END INIT INFO
case "$1" in
start)
/usr/local/bin/my-script.sh
;;
stop)
echo "Nothing to stop"
;;
*)
echo "Usage: $0 {start|stop}"
exit 1
;;
esac
Place it and register:
sudo cp my-startup /etc/init.d/
sudo chmod +x /etc/init.d/my-startup
sudo update-rc.d my-startup defaults
Only use this method if you are maintaining a legacy system that predates systemd.
Method 5: Startup Applications (GUI)
On Ubuntu Desktop, users can add startup programs through the graphical Startup Applications tool.
Open the app (search “Startup Applications” in the menu), click Add, and fill in:
- Name: My Startup Script
- Command:
/usr/local/bin/my-script.sh - Comment: (optional)
The underlying mechanism stores .desktop files in ~/.config/autostart/. You can create them manually:
[Desktop Entry]
Type=Application
Name=My Startup Script
Exec=/usr/local/bin/my-script.sh
X-GNOME-Autostart-enabled=true
GUI startup entries run after the desktop environment loads โ useful for GUI tools, but too late for system-level tasks like mounting drives or setting hardware parameters.
Comparison Table
| Method | Scope | Deps/Ordering | Restart | Logging | Ubuntu Version |
|---|---|---|---|---|---|
| systemd service | System / User | Full support | Built-in | journalctl | 15.04+ |
| cron @reboot | System / User | None | None | Manual redirect | All |
| rc.local | System only | Minimal | None | stdout to console | Pre-20.04 (legacy) |
| init.d script | System only | LSB headers | Per-script impl | syslog / file | Pre-15.04 (legacy) |
| GUI autostart | User only | None | None | stderr to journal | Desktop only |
If you need reliability, logging, and dependency ordering, use systemd. If you need quick-and-dirty for a personal machine, cron @reboot or GUI autostart may be enough.
Debugging Startup Scripts
When a script does not run at boot, work through this checklist:
Check if the script works when run manually:
sudo /usr/local/bin/your-script.sh
For systemd services, check the unit status and logs:
sudo systemctl status your-service.service
sudo journalctl -u your-service.service -b
The -b flag limits output to the current boot. Omit it to see all logs.
For cron jobs, check the cron log:
grep CRON /var/log/syslog
For rc.local, check the rc-local service status:
sudo systemctl status rc-local
Common failure modes:
- Script lacks execute permission.
- Script uses a relative path that does not exist at boot time.
PATHis missing a needed directory (e.g.,/usr/local/bin).- The service depends on a target that has not started yet โ add
After=. - A non-zero exit code stops
rc.localโ add|| true. - A cron
@rebootline misses the trailing newline โ ensure the file ends with a blank line.
Common Issues and Fixes
Script runs manually but not at boot. The environment differs โ environment variables, PATH, and working directory are all different at boot. Wrap your script to set everything it needs at the top.
Service shows “failed” status. Run journalctl -xe for a detailed error. Common causes: missing executable, bad shebang, syntax error in the script, or a missing dependency unit.
Cron @reboot does not run. The cron daemon may start after your expected time, or the script may exit too quickly. Add logging at the top of the script: exec &> /tmp/cron-debug.log.
rc.local does not run. The script file must be executable (chmod +x /etc/rc.local) and must start with #!/bin/sh -e. On Ubuntu 20.04+, the rc-local service may be masked โ run sudo systemctl unmask rc-local.
User service does not start. User services require loginctl enable-linger $USER to start at boot (not just login). Alternatively, they start when the user logs in via the display manager or SSH.
Resources
- systemd.service Documentation
- systemd.exec Documentation (Environment, WorkingDirectory)
- Ubuntu systemd Guide
- crontab(5) โ @reboot
- Arch Wiki: Autostarting
Comments