How to Stop a systemd Timer from Failing After a Reboot Due to an Unset $USER Variable

Why the $USER Variable Matters for Systemd Timers

Systemd timers are the go‑to replacement for cron on almost every distro. They’re declarative, mesh cleanly with the unit graph, and bring a bunch of goodies—persistent timers, calendar syntax, and dependency handling. The catch? If your timer’s service unit runs a script that relies on $USER, you’ll see a silent failure after a reboot. Systemd doesn’t set $USER for system‑wide units; it only populates it for user‑specific units that run in a logged‑in session.

The result is a timer that works fine while you’re logged in, but stops firing once the machine restarts. The journal will show a terse “$USER not set”, and you’ll only notice it when you dig into the logs.

Reproducing the Failure

Let’s spin up a minimal example. We want a daily backup that runs as root but needs the backup destination under the root user’s home directory. The script uses $USER to build the path.

sudo tee /usr/local/bin/daily-backup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/home/$USER/backups"
mkdir -p "$BACKUP_DIR"
tar czf "$BACKUP_DIR/$(date +%F).tar.gz" /etc
EOF
sudo chmod +x /usr/local/bin/daily-backup.sh

Now the service and timer units:

sudo tee /etc/systemd/system/daily-backup.service > /dev/null <<'EOF'
[Unit]
Description=Daily backup of /etc

[Service]
Type=oneshot
ExecStart=/usr/local/bin/daily-backup.sh
EOF

sudo tee /etc/systemd/system/daily-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Run daily backup at 02:00

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

Enable and start the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now daily-backup.timer

The first run succeeds because the timer fires while you’re logged in as root. The $USER variable is set to “root” by the login session, so the script expands /home/$USER/backups correctly.

Reboot:

sudo reboot

After the reboot, the timer triggers at 02:00, but the service dies. Check the status:

sudo systemctl status daily-backup.service

You’ll see something like:

● daily-backup.service - Daily backup of /etc
   Loaded: loaded (/etc/systemd/system/daily-backup.service; disabled; vendor preset: enabled)
   Active: failed (Result: exit-code) since Mon 2026-10-02 02:00:00 UTC; 5s ago
  Process: 12345 ExecStart=/usr/local/bin/daily-backup.sh (code=exited, status=1/FAILURE)

Oct 02 02:00:00 hostname systemd[1]: Started Daily backup of /etc.
Oct 02 02:00:00 hostname backup.sh[12345]: /usr/local/bin/daily-backup.sh: line 3: $USER: unbound variable
Oct 02 02:00:00 hostname systemd[1]: daily-backup.service: Main process exited, code=exited, status=1/FAILURE
Oct 02 02:00:00 hostname systemd[1]: daily-backup.service: Failed with result 'exit-code'.

That “unbound variable” line is the culprit.

Diagnosing with Journalctl

The first thing to do is grab the raw log:

sudo journalctl -u daily-backup.service --since today

You’ll get the same stack trace, but with timestamps and any other context. If you’re not sure which service unit is firing, list all timers:

systemctl list-timers

And if you want to see the environment that systemd gives a unit, run:

systemctl show daily-backup.service --property=Environment

You’ll notice $USER is missing. That’s the whole story.

Fixing the Problem

1. Use $HOME Instead of $USER

Systemd always sets $HOME for system units, even when running as root. Swap the script:

#!/bin/bash
set -euo pipefail
BACKUP_DIR="$HOME/backups"
mkdir -p "$BACKUP_DIR"
tar czf "$BACKUP_DIR/$(date +%F).tar.gz" /etc

Now the timer will keep working after a reboot.

2. Explicitly Export $USER

If you really need $USER for some reason, add it to the unit file:

[Service]
Environment=USER=root

or use EnvironmentFile to pull it from a file you control.

3. Run as a User‑Specific Service

If you want the job to run in the context of a logged‑in user, create a user‑specific unit under ~/.config/systemd/user/ and enable it with systemctl --user enable --now daily-backup.timer. That way $USER is populated automatically. But then you’ll need to enable systemd‑user to start at boot with systemctl enable --global user@$(id -u).service.

4. Use ExecStartPre to Set the Variable

A quick hack is to prepend a small command that sets the variable:

[Service]
ExecStartPre=/usr/bin/env USER=root
ExecStart=/usr/local/bin/daily-backup.sh

This tells systemd to export USER before launching your script.

Bottom Line

When you write a systemd timer that runs a script, don’t assume the environment is the same as your interactive shell. Systemd is deliberately minimal. $USER is not set for system units, but $HOME is. That single difference can break a backup, a sync, or any script that expects a logged‑in context. Keep an eye on the journal, check the environment with systemctl show, and adjust your unit or script accordingly.


TAGS: systemd timers shell environment


See also