LFA study materials
LFA study resources

Nightly Backup Works by Hand but Not from Cron

LFA · Put your knowledge to work in context.

All case studies

Nightly Backup Works by Hand but Not from Cron

Priya maintains a nightly backup job on an Ubuntu 22.04 server. The script is /home/priya/scripts/backup.sh, owned priya:priya with mode 750, and begins with #!/bin/bash. It creates an archive using the relative path tar czf archive/data.tgz data and appends progress lines to backup.log. Run by hand from /home/priya/scripts it succeeds every time. The user crontab entry 0 2 * * * /home/priya/scripts/backup.sh produces no archive, and a new file named backup.log appears in /home/priya containing tar: data: Cannot stat: No such file or directory. The same script also runs correctly from a system-level systemd timer whose unit sets WorkingDirectory=/home/priya/scripts and no User= (so it runs as root). Priya's interactive shell has PATH=/usr/local/bin:/usr/bin:/bin and umask 0022; cron runs jobs with a minimal environment. A colleague says the real problem is that the script is not executable and that the shebang should be changed to #!/bin/sh.

1. Diagnose the failure and evaluate the colleague's two suggestions. What is the minimal correct fix?

Read the worked answer

Cron runs each job with the working directory set to the invoking user's home, not the script's directory. Every relative path in the script therefore resolves against /home/priya: data does not exist there, so tar fails, and backup.log is created there. The stray log file in the home directory is direct evidence, and the systemd timer is a controlled comparison - the only meaningful difference is the explicit WorkingDirectory=.

Both suggestions are wrong. The file is already 750, so the executable bit is present and irrelevant. Changing the shebang to /bin/sh would pick dash on Ubuntu, silently removing bash features and creating a latent bug rather than fixing anything.

The minimal fix is to stop depending on the caller's environment: add cd "$(dirname "$0")" || exit 1 near the top, or convert every path to an absolute one. Also set an explicit PATH in the script so binaries resolve the same way under cron, systemd, and an interactive shell.

2. How would you confirm the diagnosis on the production host in under two minutes without editing the script, and what would you check next if the change did not help?

Read the worked answer

Reproduce the caller's environment rather than guessing. Run the script through a clean environment while sitting in the home directory: cd /home/priya && env -i /bin/bash --noprofile --norc -c './scripts/backup.sh', or simply (cd /home/priya && ./scripts/backup.sh). If it fails identically to cron, the working directory is confirmed as the variable. Compare with a run from the script directory. grep CRON /var/log/syslog or journalctl -u cron confirms when the job ran and as whom.

If the working directory fix does not help, check in this order: whether tar and any other binaries resolve under cron's PATH (command -v tar inside the stripped environment); whether tar's output path is writable by priya, since earlier root runs may have left root-owned files in the archive directory; whether the archived source files are readable by priya; and whether the job is being killed for time or output volume. Add a temporary pwd; id; env header to the log to capture the real runtime context.

3. After the working-directory fix, the nightly job starts failing with 'Permission denied' on the archive directory. Explain the cause and how you would restructure the two jobs.

Read the worked answer

The system-level timer has no User=, so it runs as root and creates data.tgz and any new directories as root. Priya's cron job, running as priya, cannot write into or replace those files. Adding a working directory fixed the path problem but exposed an ownership conflict that only appears once the two jobs touch the same target.

Fix ownership once with sudo chown -R priya:priya /home/priya/scripts/archive and sudo chmod 750, then make the systemd unit run as the same principal by adding User=priya and Group=priya (or drop the root job entirely and keep one). Running the same task under two identities against one destination guarantees this class of bug, so consolidate to a single scheduled job owned by priya.

Add flock -n /run/backup.lock /home/priya/scripts/backup.sh so overlapping runs cannot corrupt the archive, and note that a root cron job writing into a user's home is also a privilege-hygiene problem worth removing.