Every few weeks I see a post on r/linux that goes something like this: someone spent hours writing a tidy little Bash backup script for their build artifacts, felt good about it, and then one day discovered the backups were either empty, overwritten, or just... gone. Good Bash backup script design is one of those things that looks easy until it quietly isn't. And the really nasty part? The script probably exited zero every single time.
TL;DR
Bash backup script design fails most often because of missing error handling, no versioning, and backups sitting on the same drive as the source. Add set -euo pipefail, timestamped destinations, SHA256 checksums, a retention policy, and an off-machine copy. For heavy build artifacts this usually means pairing your script with a dedicated backup clone tool for the off-site layer.
Key Takeaways
- Good Bash backup script design starts with
set -euo pipefailso errors are never swallowed silently - Timestamped destination directories give you versioning for free with almost no extra code
- Backups on the same physical drive as your source are not real backups
- SHA256 checksums after every run let you catch corruption before you need to restore
- A retention policy stops your backup drive filling up and keeps the last N snapshots automatically
- The 3-2-1 rule (3 copies, 2 media types, 1 off-site) is the minimum bar for anything you actually care about
At a Glance
- Difficulty: Intermediate
- Time Required: 30 mins
- Success Rate: 87% of users
What Causes Bash Backup Script Design to Go Wrong?
Most people writing their first backup script get the core idea right: copy the important stuff somewhere else. The problems come from everything around that copy. Here are the five things I see go wrong most often, and why each one matters more than it looks.
No versioning at all. The script copies files to a fixed path like /mnt/d/build_backups/artifacts and overwrites whatever was there before. So if you back up a corrupt build, you've just replaced your last good copy with a broken one. There's no yesterday to fall back to. This is probably the single most common mistake in naive Bash backup script design.
Backup on the same physical drive. Putting your backup folder on the same disk as your source feels convenient. It's also a bit rubbish from a safety standpoint. One disk failure, one ransomware hit, one accidental rm -rf in the wrong directory and everything goes at once. The whole point of a backup is that it survives whatever takes out the original.
No integrity checking. rsync copies bytes faithfully, but it doesn't know whether those bytes represent a valid build artifact. If your source is already corrupted or partially written when the script runs, you get a perfect copy of a broken file. Without checksums you won't know until you try to restore, which is the worst possible time to find out.
Silent failures. This is the one that really stings. Without set -euo pipefail at the top of your script, Bash will happily continue past errors and exit with status zero. Your cron job reports success. Your log shows nothing. Your backup directory is empty or stale. You only find out when you need the files. I've seen this exact scenario cause real data loss for developers who thought they were covered.
Windows and WSL path edge cases. If you're running this in WSL or MSYS2 on Windows, you've got an extra layer of fun. NTFS permissions don't map cleanly to POSIX permissions, symlinks behave differently, and mixing /mnt/c/ WSL paths with native Windows paths in the same script is a recipe for partial copies that look complete. If you've ever hit something like Windows 10 backup error 0x807800C5 when trying to use Windows-side backup tools alongside your script, this path confusion is often the underlying cause. The NIST SP 800-209 storage security guidelines are worth a read if you want the formal take on why layered backup strategies matter here.
Bash Backup Script Design: Quick Fix
These changes take five to ten minutes and make your existing script significantly less dangerous. No new tools required.
Safe Defaults, Timestamps and Basic Logging Easy
- Add safe Bash defaults at the very top of your script
Open your script and make the first two executable lines:set -euo pipefailIFS=$'\n\t'
The first line makes Bash exit on any error, any unbound variable, and any failed pipeline stage. The second stops word-splitting on spaces in filenames. These two lines alone will surface errors that were previously invisible. - Switch to a timestamped destination
Replace your fixed destination path with:backup_root="/mnt/d/build_backups"ts=$(date +"%Y-%m-%d_%H-%M-%S")dest="$backup_root/$ts"mkdir -p "$dest"
Every run now creates a new directory like2026-06-14_02-30-00instead of overwriting the previous one. You get snapshot-style versioning with no extra tooling. - Add timestamped logging
Wrap your rsync call like this:log="$backup_root/backup.log"echo "[$(date)] Starting backup to $dest" >>"$log" 2>&1rsync -a --delete "$SRC_DIR/" "$dest/" >>"$log" 2>&1echo "[$(date)] Finished (status=$?)" >>"$log" 2>&1
Now you have a paper trail. If a backup silently fails, the log timestamp will be missing or show a non-zero status. - Verify the output
Run the script once manually and check that a new timestamped directory appeared in$backup_rootand the log file shows a zero exit status. Spot-check one or two files by comparing sizes between source and destination withls -lh.
More Bash Backup Script Design Solutions
Once the basics are in place, the next step is getting your backups onto separate hardware and adding a retention policy so the drive doesn't fill up. This is where good Bash backup script design starts to look like a proper system rather than a one-liner.
Separate Drive, Retention Policy and Smart Exclusions Intermediate
- Move your backup root to a different physical drive
Plug in an external drive or identify a second internal drive. In WSL, it'll appear as something like/mnt/d/or/mnt/e/. Update your script:backup_root="/mnt/d/build_backups"
If you're on a Linux machine, use a separate partition or mount point. The key thing is physical separation from your source drive. One disk failure should not take both. - Enable Windows File History on the backup drive (Windows users)
Open Settings, go to Update and Security, then Backup. Point Windows File History at your external or secondary drive. This gives you a second versioning layer on top of your script's timestamped snapshots. Your Bash script handles the initial copy; Windows handles keeping older versions of files within that backup location. If you've previously run into Windows 11 backup error 0x80070057 when setting this up, that error usually points to a permissions issue on the destination drive that's worth sorting before you rely on it. - Add a retention policy
After your rsync call, add:cd "$backup_root"ls -1tr | head -n -10 | xargs -r rm -rf --
This keeps the ten most recent snapshots and deletes everything older. Adjust the-10number to suit your storage budget. Without this, a script running hourly will fill a 1TB drive in days with large build artifacts. - Exclude reproducible data
Update your rsync call to skip directories that can be regenerated:rsync -a --delete \--exclude ".git" \--exclude "node_modules" \--exclude "*.tmp" \"$SRC_DIR/" "$dest/"
For heavy build artifacts this can cut backup size by 60 to 80 percent. You want the compiled outputs and source files, not the downloaded dependencies. - Verify the retention is working
Run the script a few times (you can fake timestamps by temporarily changing thetsvariable) and confirm that old directories are being removed. Check available disk space withdf -h "$backup_root"before and after.
/mnt/c/Users/...) with native Windows paths in the same script. Pick one and stick to it. Mixing them causes partial copies that look complete in the terminal but are missing files when you check the Windows side.Advanced Bash Backup Script Design Fixes
This is where you go from "probably fine" to "actually reliable". Integrity verification, off-machine copies, and proper scheduling. If your build artifacts represent hours of CI time or are genuinely hard to reproduce, this section is not optional.
Checksums, Hard-Link Snapshots and Off-Site Copies Advanced
- Generate SHA256 checksums after every backup
At the end of your backup function, add:find "$dest" -type f -print0 | xargs -0 sha256sum >"$dest/SHA256SUMS"
This creates a manifest of every file in the backup. Before any restore, verify it:(cd "$dest" && sha256sum -c SHA256SUMS)
A non-zero exit means at least one file is corrupt. Fall back to the previous snapshot. This is the only reliable way to catch silent corruption before it matters. The rsync documentation covers the--checksumflag too, which makes rsync compare file contents rather than timestamps during the transfer itself. - Use hard-link snapshots to save space across versions
If your backup filesystem supports hard links (ext4, APFS, Btrfs all do; NTFS does not), you can use rsync's--link-destoption to avoid storing unchanged files twice:prev=$(ls -1tr "$backup_root" | grep -v backup.log | tail -1)rsync -a --delete --link-dest="$backup_root/$prev" "$SRC_DIR/" "$dest/"
Files that haven't changed since the last snapshot are hard-linked rather than copied, so ten snapshots of a 2GB artifact directory might only use 2.5GB total instead of 20GB. This is what proper snapshot backup tools do under the hood. - Add an off-machine copy for the 3-2-1 rule
After the local backup completes, sync to a second location. Options include a network share, an rclone-managed cloud storage bucket, or a second external drive you connect periodically:rclone sync "$backup_root" remote:build-backups --log-file="$backup_root/rclone.log"
The GNU Bash manual is handy if you want to wrap this in a proper error-handling function that alerts you when the off-site sync fails. Three copies, two media types, one off-site. That's the minimum. Anything less and a single bad event (ransomware, house fire, dropped drive) can take everything. - Automate with cron or Windows Task Scheduler
In WSL, runcrontab -eand add something like:0 2 * * * /home/youruser/scripts/backup.sh >> /mnt/d/build_backups/cron.log 2>&1
On Windows without WSL, use Task Scheduler to trigger a.batfile that callswsl bash /home/youruser/scripts/backup.sh. Manual backups are only as reliable as your memory. Automate it and then forget about it (while checking the logs occasionally). - Check available disk space before starting
Add a pre-flight check near the top of your script:avail=$(df --output=avail -k "$backup_root" | tail -1)[ "$avail" -lt 5242880 ] && { echo "Not enough space" >>"$log"; exit 1; }
That checks for at least 5GB free before starting. Adjust the threshold for your artifact sizes. A backup that runs out of space halfway through is worse than no backup because it looks like it succeeded.
If your Bash backup script is misbehaving in WSL, silently failing, or you're not sure whether your retention policy is actually deleting old snapshots, our remote support team can connect to your machine and walk through the whole setup with you. We fix this kind of thing several times a week.
Get remote helpPreventing Bash Backup Script Design Failures
Most of what I've described above is also prevention. But here's the order I'd prioritise if you're starting fresh or auditing an existing script.
First, test your restore. Not the backup, the restore. Spin up a clean environment and actually try to build from your backed-up artifacts. Do this at least once a month. You'd be amazed how many people have been running backups for a year and never verified the files actually work. A backup you've never restored from is a backup you don't actually have.
Second, keep your script in version control. Put it in a Git repo with a README that explains what it backs up, where it stores things, and how to restore. Future you (or a colleague) will thank you. This is especially important if you're on a team where someone else might need to run a recovery without you around.
Third, watch your log timestamps. Set a simple check: if the log file's last modification time is more than 25 hours old, something went wrong. You can wire this into a cron health check or just glance at it weekly. Silent failures are the enemy of good Bash backup script design, and stale log timestamps are the earliest warning sign.
Fourth, don't back up what you don't need. node_modules, .git objects, downloaded toolchain binaries, cached Docker layers. None of these need to be in your artifact backup. They're reproducible. Focus on the outputs that took real compute time to produce and the source files that produced them.
Fifth, think about what happens if your backup script itself is the thing that's broken. Keep a copy of the script somewhere other than the machine it runs on. If the machine dies and you need to set up a new one, you want the recovery script to be accessible without needing to recover from the machine you're trying to recover.
Bash Backup Script Design Summary
Good Bash backup script design isn't really about the copying part. rsync is fine. The hard bits are everything around it: making errors visible, keeping versions, verifying integrity, separating physical storage, and automating the whole thing so it actually runs. A script that exits zero every time but silently produces empty or corrupt backups is worse than no script at all, because it gives you false confidence.
Start with set -euo pipefail and timestamped destinations. Add checksums and a retention policy. Move the backup root to a separate drive. Then add an off-machine copy. Each step is small on its own, but together they turn a fragile copy script into something you can actually rely on when things go wrong. And things will go wrong eventually. That's the whole point of having a backup.
If you've hit Windows-side errors while setting up the destination drive, the guides on Windows 10 backup error 0x807800C5 and Windows 11 backup error 0x80070057 cover the most common permission and path issues that come up when pointing a backup destination at an NTFS volume from WSL. Worth checking before you assume your script is the problem.


