Running a nightly backup script after logout with systemd user units – no nohup needed
I set up a little home‑lab server last year and wanted a way to snap my home directory every night without tying the job to a terminal. The old nohup trick works, but it leaves orphaned processes and you end up chasing logs in the dark. Systemd user units give a cleaner, more reliable solution that plays nicely with the rest of the system.
Why use a systemd user unit?
- Automatic cleanup – When you log out, systemd kills any processes started by the unit, so you never end up with stray backup jobs.
- Fine‑grained control – You can set
OnCalendar,ExecStart,Restart, andStandardOutputjust like a system service. - No extra daemons – The user instance of systemd is already running for every logged‑in user, so you don’t need to start a separate service manager.
- Security isolation – The unit runs in the user namespace, limiting what it can see and do compared to a root‑level service.
Quick setup
Assume your backup script sits at ~/scripts/backup.sh and produces its own logs.
chmod +x ~/scripts/backup.sh
1. Create the unit file
User units live in ~/.config/systemd/user/. Drop a file called backup-nightly.service there:
[Unit]
Description=Nightly backup of home directory
After=network-online.target
[Service]
Type=oneshot
ExecStart=/home/youruser/scripts/backup.sh
StandardOutput=append:/home/youruser/backup.log
StandardError=append:/home/youruser/backup.err
# If you want the job to retry on failure
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=default.target
After=network-online.targetmakes sure the network is up before the backup starts.StandardOutput/StandardErrorsend output straight to log files;append:keeps old data.Restart=on-failureis optional; if the script exits non‑zero, systemd will retry after 5 s.
2. Create a timer unit
The timer triggers the service. In the same directory, create backup-nightly.timer:
[Unit]
Description=Run nightly backup at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true guarantees the job runs if the machine was off at the scheduled time.
3. Reload, enable, and start
systemctl --user daemon-reload
systemctl --user enable --now backup-nightly.timer
Verify the timer:
systemctl --user list-timers | grep backup-nightly
The first run will happen at the next 02:30. After that, systemd takes care of the rest.
What the backup script should look like
A minimal example that uses rsync:
#!/usr/bin/env bash
set -euo pipefail
SRC="$HOME/"
DST="/mnt/backup/home-$(date +%Y%m%d)/"
mkdir -p "$DST"
rsync -aAXv --delete "$SRC" "$DST"
-aAXvpreserves permissions, ACLs, and extended attributes.--deletekeeps the destination clean.- The script exits with a non‑zero status if
rsyncfails, triggering theRestart=on-failurelogic.
If you prefer btrfs snapshots, swap the rsync line for a btrfs subvolume snapshot command. The unit file stays the same.
Security considerations
- Least‑privilege – The unit runs as your user, not root. If the backup needs to touch root‑only files, add a dedicated
sudorule that only allows the script to run as root. - File permissions – Keep the script and logs readable only by you:
chmod 700 backup.shandchmod 600 backup.log backup.err. - Network – If the backup writes to a remote server via SSH, use key‑based authentication and restrict the key’s permissions (
chmod 600 ~/.ssh/id_rsa). - Audit – Log the start and end times inside the script. A simple
echo "$(date) – backup finished" >> ~/backup.loghelps spot silent failures.
Common pitfalls and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Timer never fires | systemd --user not started |
Log in via a graphical session or run systemctl --user enable --now after login. |
| Service fails with “Permission denied” | Script or log file permissions | Ensure the script is executable and logs are writable. |
| Backup runs but no data copied | Wrong source path | Verify $SRC expands correctly; add echo "$SRC" to the script for debugging. |
| Logs grow unchecked | StandardOutput set to append: |
Rotate logs with logrotate or change to StandardOutput=journal to use systemd’s journal. |
Use journalctl --user -u backup-nightly.service to inspect recent runs. The -b flag shows only the current boot, which is handy for debugging after a reboot.
Trade‑offs compared to nohup
| Feature | nohup |
Systemd user unit |
|---|---|---|
| Process cleanup | Manual kill |
Automatic on logout |
| Logging | Manual redirection | Built‑in StandardOutput |
| Scheduling | cron or at |
systemd.timer |
| Security isolation | None | User namespace |
| Resource limits | None | LimitCPU, LimitFSIZE, etc. |
If you only need a one‑off backup, nohup is fine. For recurring jobs, systemd units are more maintainable and integrate with the rest of the system.
Happy backing up, and keep your logs tidy!
See also
- I lost SSH access to my home server after accidentally setting /home/user to 777 – how I restored it with a live USB
- When pulseaudio turns your laptop into a CPU hamster: how to reduce its load with a simple configuration tweak
- Pulling the top 10 HTTP status codes from an Nginx access log with awk
- Restoring a Half‑Completed Borg Backup That Stopped at 50%
- I kept my home server from going down during Let’s Encrypt renewal – here’s the one‑liner timer I added.