Automated Backup Script on Linux

Write a bash script that backs up, rotates, and schedules with cron.

Introduction

30 Second Summary

Every file on your computer is one accidental deletion or hardware failure away from being gone forever. Professional system administrators prevent this by automating backups that run silently in the background, compressing important files and cleaning up old copies without any manual effort.

In this project, you will write a Bash script from scratch that compresses a directory into a timestamped archive, enforces a retention policy to delete old backups, logs every action, and runs automatically on a daily schedule.

What You'll Build

A production-ready backup automation system for Linux that compresses files, rotates old archives, and runs on a cron schedule with full logging.

By the end of this project, you'll have:

  • A backup.sh script that creates compressed, timestamped archives of any target directory.
  • An automated 7-day retention policy that deletes old backups and keeps your storage clean.
  • A cron job running the script daily at 2:00 AM with all activity recorded in a log file.
  • Secret Mission: Add a pre-backup disk space safety check and post-backup integrity verification.

Are there any prerequisites?

You need access to a Linux environment. This can be a local Linux machine, a virtual machine, or WSL (Windows Subsystem for Linux) on Windows. No prior scripting experience is required.

Before We Start

Before We Start...

Set Up Your Project Structure

Every backup system needs a clean foundation before any logic gets written. You need a project directory, sample data to practice backing up, and the script file itself.

In this step, you will create the full project structure, populate it with test files, and prepare your Bash script for coding in the next step.

In this step, get ready to:
  • Create the project directory and sample data to back up.
  • Create the backup destination directory.
  • Create the script file and make it executable.
Create the project directory and sample data

Your backup project needs a home base that holds the script, the data you want to protect, and the archived backups themselves. The sample data gives you something real to back up while testing.

  • Create the project directory and sample-data subdirectory by running this command in your terminal:
mkdir -p ~/backup-project/sample-data

What does this command do?

The -p flag tells mkdir to create the full path of directories in one go. It also avoids throwing an error if the directory already exists.

  • Add sample files to the sample-data directory by running these commands:
echo "Important document content" > ~/backup-project/sample-data/file1.txt
echo "Project notes and data" > ~/backup-project/sample-data/file2.txt
echo "Configuration settings" > ~/backup-project/sample-data/file3.txt
  • Confirm the files exist by running:
ls ~/backup-project/sample-data

You should see file1.txt, file2.txt, and file3.txt listed in the output.

Don't see the files?

Make sure you typed the path exactly as shown. The tilde (~) refers to your home directory. If you copied the commands and see a "No such file or directory" error, double check that the mkdir command ran first without errors.

Help me troubleshoot why my sample-data directory is empty or missing.

Create the backup destination directory

Your script needs a dedicated folder to store the compressed archives it creates. Keeping backups separate from source data makes cleanup and rotation straightforward.

  • Create the backups directory by running:
mkdir -p ~/backup-project/backups
  • Verify the full project structure by running:
ls ~/backup-project

You should see three items: backups, sample-data, and nothing else yet. The script file comes next.

Create the script file and make it executable

A Bash script needs two things before it can run: the shebang line that tells the system which interpreter to use, and execute permission on the file itself.

  • Create backup.sh with the shebang line by running:
echo '#!/bin/bash' > ~/backup-project/backup.sh
  • Make the script executable by running:
chmod +x ~/backup-project/backup.sh

What does chmod +x do?

The chmod +x command adds execute permission to a file. Without it, Linux treats the file as plain text and refuses to run it as a script.

The shebang line (#!/bin/bash) tells the operating system to use Bash as the interpreter when you run the script directly.

  • Verify the script was created and has execute permission by running:
ls -l ~/backup-project/backup.sh

You should see something like -rwxr-xr-x at the start of the output. The x characters confirm execute permission is set.

  • Confirm the file content is correct by running:
cat ~/backup-project/backup.sh

You should see #!/bin/bash as the only line.

Seeing "Permission denied" or an empty file?

If chmod +x gives a "No such file or directory" error, the echo command above may not have run successfully. Try running the echo command again, then run chmod +x again.

If cat shows an empty file, make sure you used single quotes around #!/bin/bash in the echo command. The ! character has special meaning in Bash when inside double quotes.

Help me fix permission or file creation issues with my backup script.

✔️ Awesome, I've got everything!

Great. Double check you see all three items in your project folder by running:

ls ~/backup-project

You should see: backups, backup.sh, and sample-data.

ⓧ I'd like to double check the full code

Here is what backup.sh should contain at this point:

#!/bin/bash

And here is the full project structure you should have:

~/backup-project/
├── backup.sh          (executable, contains shebang)
├── backups/           (empty, will store archives)
└── sample-data/
    ├── file1.txt
    ├── file2.txt
    └── file3.txt

Your project structure is ready. Next up, you will write the core backup logic that compresses your sample data into a timestamped archive.

Write the Backup Script Core

Your project structure is ready with directories and an empty script file in place. Now it's time to write the actual backup logic that makes this script useful.

The core of any backup script handles three things: knowing what to back up, creating the compressed archive, and handling errors gracefully. In this step, you'll add all three to backup.sh.

In this step, get ready to:
  • Add configuration variables that control the script's behaviour.
  • Write a logging function and input validation checks.
  • Write the tar compression command that creates a timestamped archive.
Add the shebang line and configuration variables

Every well-structured script starts with a configuration section at the top. This gives you a single place to change paths and settings without digging through the logic below.

  • Open backup.sh in nano by running this command:
nano backup.sh

You should see the #!/bin/bash line already at the top of the file.

  • Position your cursor on the blank line below #!/bin/bash, then type the following configuration block:
# ============================================
# Automated Backup Script with Rotation
# ============================================

# --- Configuration ---
SOURCE_DIR="/home/$USER/backup-project/sample-data"
BACKUP_DIR="/home/$USER/backup-project/backups"
LOG_FILE="/home/$USER/backup-project/backup.log"
RETENTION_DAYS=7
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_NAME="backup_${DATE}.tar.gz"

What do these variables do?

  • SOURCE_DIR points to the directory you want to back up (your sample data).
  • BACKUP_DIR is where compressed archives will be stored.
  • LOG_FILE records every action the script takes with timestamps.
  • RETENTION_DAYS controls how many days of backups to keep (7 days).
  • DATE captures the current timestamp in a filename-safe format using date +%Y-%m-%d_%H-%M-%S.
  • BACKUP_NAME combines the word "backup" with the timestamp and the .tar.gz extension.
  • Save your progress by pressing Ctrl+O, then press Enter to confirm the filename.

You won't see a change outside nano yet. The next chunk uses these variables to build the actual backup logic.

Write the logging function and input validation

Before the script does any real work, it needs two safety features. A logging function records what happens (useful when the script runs unattended), and input validation stops the script early if something is wrong.

  • In nano, add an empty line below the BACKUP_NAME line, then type the following code:
# --- Functions ---
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# --- Validation ---
if [ ! -d "$SOURCE_DIR" ]; then
    log "ERROR: Source directory does not exist: $SOURCE_DIR"
    exit 1
fi

mkdir -p "$BACKUP_DIR"

What does this code do?

The log() function prepends a timestamp to any message and writes it to both the terminal and the log file using tee -a. The -a flag means "append" so it never overwrites previous log entries.

The validation block checks whether SOURCE_DIR actually exists before doing anything. If it doesn't exist, the script logs an error and exits with code 1 (meaning failure). Then mkdir -p ensures the backup destination directory exists, creating it (and any parent directories) if needed without erroring if it already exists.

  • Save again with Ctrl+O and press Enter.

You won't see a change outside the editor yet. The next chunk adds the actual backup command that ties everything together.

Write the tar compression command

This is the heart of the script. The tar command compresses your source directory into a single .tar.gz archive, and the exit code check tells you whether it worked.

  • In nano, add an empty line below mkdir -p "$BACKUP_DIR", then type the following code:
# --- Create Backup ---
log "Starting backup of $SOURCE_DIR"

tar -czf "$BACKUP_DIR/$BACKUP_NAME" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

if [ $? -eq 0 ]; then
    log "SUCCESS: Backup created -> $BACKUP_DIR/$BACKUP_NAME"
else
    log "ERROR: Backup failed for $SOURCE_DIR"
    exit 1
fi

What does this code do?

The tar -czf command creates (-c) a gzip-compressed (-z) archive file (-f). The -C flag changes to the parent directory first so the archive stores relative paths instead of full absolute paths.

The $? variable holds the exit status of the last command. A value of 0 means success. If tar fails for any reason, the script logs the error and exits immediately instead of continuing with a broken backup.

  • Save and exit nano by pressing Ctrl+O, Enter, then Ctrl+X.
  • Run the script to test it by running this command:
./backup.sh

You should see a log message printed to the terminal confirming the backup was created, something like SUCCESS: Backup created -> /home/.../backups/backup_2026-06-05_14-30-22.tar.gz.

  • Confirm the archive exists in your backups folder by running:
ls backups/

You should see a file named backup_ followed by today's date and time, ending in .tar.gz.

Don't see a backup file?

  • If you see "Permission denied", make sure the script is executable. Run chmod +x backup.sh and try again.
  • If you see "Source directory does not exist", double-check that sample-data exists inside your backup-project folder. Run ls to verify.
  • If you see a syntax error, reopen backup.sh in nano and compare your code against the full version below.

Help me debug my backup script

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that the backup archive exists in backups/.

ⓧ I'd like to double check the full code

Here's the complete backup.sh as it should look at the end of this step:

#!/bin/bash

# ============================================
# Automated Backup Script with Rotation
# ============================================

# --- Configuration ---
SOURCE_DIR="/home/$USER/backup-project/sample-data"
BACKUP_DIR="/home/$USER/backup-project/backups"
LOG_FILE="/home/$USER/backup-project/backup.log"
RETENTION_DAYS=7
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_NAME="backup_${DATE}.tar.gz"

# --- Functions ---
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# --- Validation ---
if [ ! -d "$SOURCE_DIR" ]; then
    log "ERROR: Source directory does not exist: $SOURCE_DIR"
    exit 1
fi

mkdir -p "$BACKUP_DIR"

# --- Create Backup ---
log "Starting backup of $SOURCE_DIR"

tar -czf "$BACKUP_DIR/$BACKUP_NAME" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

if [ $? -eq 0 ]; then
    log "SUCCESS: Backup created -> $BACKUP_DIR/$BACKUP_NAME"
else
    log "ERROR: Backup failed for $SOURCE_DIR"
    exit 1
fi

Your script can now create compressed, timestamped backups on demand. Next up, you'll add automatic rotation to delete old backups and put the logging through a full test.

Add Rotation and Logging

Your script can now create compressed, timestamped backups. But there is a problem. Without cleanup, those archives will pile up forever and eventually fill your disk.

In this step, you will add a retention policy that automatically deletes backups older than 7 days. You will also add completion logging so every run leaves a clear audit trail.

In this step, get ready to:
  • Add a retention policy that finds and deletes old backups.
  • Add conditional logging that records what was cleaned up.
  • Test the complete script manually and verify the log output.
Add the rotation block with conditional logging

The find command can search for files by age and delete them in one pass. You will use it to enforce your 7-day retention policy.

  • Open backup.sh in nano by running this command:
nano ~/backup-project/backup.sh
  • Scroll to the bottom of the file. Find the closing fi that ends the tar success/failure check.
  • Place your cursor on the empty line after that fi and add the following rotation block:
# --- Rotation ---
log "Rotating backups older than $RETENTION_DAYS days"

DELETED=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -print -delete)

if [ -n "$DELETED" ]; then
    log "Deleted old backups: $DELETED"
else
    log "No backups older than $RETENTION_DAYS days to remove"
fi

log "Backup complete. Current backups:"
ls -lh "$BACKUP_DIR"/backup_*.tar.gz >> "$LOG_FILE" 2>/dev/null

log "---"

What does this code do?

The find command searches your backup directory for .tar.gz files with a modification time older than 7 days. The -print -delete flags output the filenames AND remove them in one pass.

The output of find is captured into the DELETED variable. The if [ -n "$DELETED" ] check tests whether the variable is non-empty. If files were deleted, their names get logged. If nothing was old enough to remove, a "no backups to remove" message is logged instead.

The final ls -lh writes a listing of all remaining backups directly into the log file. The log "---" line adds a visual separator between runs so the log stays readable over time.

Why -mtime instead of counting files?

There are two common rotation strategies: age-based (delete files older than N days) and count-based (keep only the last N files). Age-based rotation with -mtime is more robust because it works correctly even when backups run at irregular intervals or multiple times per day.

  • Save the file by pressing Ctrl+O, then Enter to confirm.
  • Exit nano by pressing Ctrl+X.

Seeing a syntax error when you save?

Check that the if [ -n "$DELETED" ]; then line has spaces inside the square brackets. Bash requires a space after [ and before ].

Also verify that the block ends with fi on its own line. A missing fi causes unexpected end-of-file errors.

Help me debug a syntax error in my backup rotation script.

Test your complete backup script

Your script is now feature-complete. It compresses, rotates, and logs. Time to run it and confirm everything works together.

  • Make sure you are in the project directory by running:
cd ~/backup-project
  • Run the backup script by executing:
./backup.sh

You should see timestamped log lines printed directly in your terminal. This is because the tee -a command writes to both the screen and the log file simultaneously.

What should I see?

Your terminal output should show lines like: a "Starting backup" message, a "SUCCESS: Backup created" confirmation, a "Rotating backups" message, a "No backups older than 7 days to remove" message (since this is your first run), and a "Backup complete" line.

  • Verify the backup archive was created by running:
ls -lh backups/

You should see at least one file named something like backup_2026-06-05_14-30-22.tar.gz (the timestamp will match when you ran the script).

  • Check that the log file was created and contains the full record by running:
cat backup.log

You should see every log entry from your test run, including the final listing of backup files and the --- separator at the end.

Script runs but no backup file appears?

Check that your sample-data directory exists and contains at least one file. The tar command needs something to compress.

If you see "Permission denied", confirm the script is executable by running chmod +x backup.sh again.

Help me figure out why my backup script runs but does not create an archive.

✔️ Awesome, I've got everything!

Great. Double check you have saved backup.sh and confirmed a backup archive exists in your backups/ directory.

ⓧ I'd like to double check the full code

Here is the complete backup.sh file as it should look at this point:

#!/bin/bash

# ============================================
# Automated Backup Script with Rotation
# ============================================

# --- Configuration ---
SOURCE_DIR="/home/$USER/backup-project/sample-data"
BACKUP_DIR="/home/$USER/backup-project/backups"
LOG_FILE="/home/$USER/backup-project/backup.log"
RETENTION_DAYS=7
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_NAME="backup_${DATE}.tar.gz"

# --- Functions ---
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# --- Validation ---
if [ ! -d "$SOURCE_DIR" ]; then
    log "ERROR: Source directory does not exist: $SOURCE_DIR"
    exit 1
fi

mkdir -p "$BACKUP_DIR"

# --- Create Backup ---
log "Starting backup of $SOURCE_DIR"

tar -czf "$BACKUP_DIR/$BACKUP_NAME" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

if [ $? -eq 0 ]; then
    log "SUCCESS: Backup created -> $BACKUP_DIR/$BACKUP_NAME"
else
    log "ERROR: Backup failed for $SOURCE_DIR"
    exit 1
fi

# --- Rotation ---
log "Rotating backups older than $RETENTION_DAYS days"

DELETED=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -print -delete)

if [ -n "$DELETED" ]; then
    log "Deleted old backups: $DELETED"
else
    log "No backups older than $RETENTION_DAYS days to remove"
fi

log "Backup complete. Current backups:"
ls -lh "$BACKUP_DIR"/backup_*.tar.gz >> "$LOG_FILE" 2>/dev/null

log "---"

Your backup script is fully functional. It compresses, rotates, and logs every action. Next up, you will schedule it with cron so it runs automatically every day without you lifting a finger.

Schedule with Cron

Your backup script works perfectly when you run it by hand. The problem is that manual backups rely on you remembering to run the command every single day.

Linux has a built-in job scheduler called cron that runs commands on a repeating schedule. In this step, you will register your script with cron so it executes automatically every day at 2:00 AM without any manual intervention.

In this step, get ready to:
  • Open the crontab editor and add a scheduled entry.
  • Verify the cron job is installed.
  • Simulate a scheduled run to confirm end-to-end operation.
Open the crontab editor

The crontab is a per-user file that tells cron which commands to run and when. You edit it with a dedicated command that validates your changes automatically on save.

  • Open your crontab for editing by running this command:
crontab -e

What does crontab -e do?

This command opens your personal crontab file in a text editor. Any changes you save are automatically installed as scheduled jobs. You do not need sudo because each user has their own crontab.

First time using crontab?

If this is your first time running crontab -e, your system may ask you to select an editor. Choose the option marked nano (usually option 2). Nano is the simplest terminal text editor for beginners.

Add the cron entry for daily 2:00 AM backups

Cron uses a five-field time expression followed by the command to run. Each field controls a different unit of time.

  • Scroll to the bottom of the crontab file (below any comment lines starting with #).
  • Add this line at the very end of the file:
0 2 * * * /home/[[USERNAME="your Linux username"]]/backup-project/backup.sh

What does 0 2 * * * mean?

The five fields before the command represent minute, hour, day-of-month, month, and day-of-week. Here is what each value means:

  • 0 = at minute 0 (the top of the hour).
  • 2 = at hour 2 (2:00 AM).
  • * = every day of the month.
  • * = every month.
  • * = every day of the week.

Together, 0 2 * * * means "run once per day at exactly 2:00 AM."

Use your actual home path

Replace the username placeholder with your actual Linux username. You can confirm it by running echo $HOME in a separate terminal. The path must be absolute because cron does not know your current working directory.

  • Save the file and exit the editor. In nano, press Ctrl+O to save, then Ctrl+X to exit.

You should see a message saying the new crontab has been installed.

Seeing an error about the crontab file?

If you see a syntax error message, cron detected an invalid line. Re-open the file with crontab -e and check that your entry has exactly five time fields separated by spaces before the full path to the script.

Help me fix my crontab entry

Why cron over systemd timers?

Cron is universally available across all Linux distributions and has been the standard scheduling tool for decades. Systemd timers are a newer alternative but add complexity and are not available on every system.

For a backup script that runs once a day, cron is the simplest and most portable choice.

Verify the cron job and simulate a run

Your cron job will not fire until 2:00 AM. You can verify it is registered right now and then simulate what cron will do by running the script manually.

  • Confirm your cron job is installed by running this command:
crontab -l

You should see your 0 2 * * * entry followed by the full path to backup.sh listed in the output.

Don't see your cron entry?

If crontab -l shows nothing or says no crontab is installed, the file was not saved correctly. Run crontab -e again, add the entry, and make sure you save before exiting.

My crontab -l is not showing my cron entry

  • Simulate what cron will do at 2:00 AM by running the script manually. Navigate to your project directory first:
cd ~/backup-project
bash backup.sh
  • Check the log file to confirm the new run was recorded:
cat backup.log

You should see a fresh set of timestamped log entries at the bottom showing a successful backup creation and rotation check.

  • Confirm the new archive exists in the backups directory:
ls -lh backups/

You should see multiple backup_*.tar.gz files listed, each with a different timestamp.

What happens at 2:00 AM now?

Every day at 2:00 AM, cron will execute your script with the same result you just saw. It compresses sample-data into a timestamped archive, removes any backups older than 7 days, and logs everything to backup.log. No manual intervention required.

Script works manually but worried about cron?

The most common issue with cron jobs is incorrect paths. Your script uses $USER and $HOME which cron sets automatically from your system profile. If you ever need to debug a cron job, check the system mail with cat /var/mail/$USER for error output.

Help me debug my cron backup job

Your backup system is now fully automated. Every night at 2:00 AM, cron will compress your data, clean up old archives, and log the results. Next up, take on the Secret Mission to add disk space safety checks and backup integrity verification.

Secret mission

Disk Space Check and Backup Verification

Your backup script works, but what happens if the disk is almost full? Or if the archive gets corrupted mid-write? Production scripts need safety checks that catch problems before they cause damage.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your Linux machine, so there are no ongoing cloud costs. However, the cron job you created will continue running daily at 2:00 AM, creating new backup archives. Decide whether to keep it running, pause it, or delete everything.

Resources you used:

  • The backup-project directory at /home/$USER/backup-project/ (contains backup.sh, sample-data, backups, and backup.log).
  • A cron job entry running backup.sh daily at 2:00 AM.

Keep everything running

No action needed. Choose this if you want backups to keep running daily. Your cron job will continue creating timestamped archives every day at 2:00 AM.

  • The cron job runs automatically and stores new archives in /home/$USER/backup-project/backups/.
  • The rotation policy deletes backups older than 7 days, so disk usage stays bounded.
  • Check backup.log periodically to confirm runs are succeeding.

Pause - I'll come back to this later

Stop the scheduled backups to free up background activity, but keep all your files so you can resume later.

  • Open your crontab for editing by running:
crontab -e
  • Find the line that starts with 0 2 * * * and delete it.
  • Save and exit the editor.
  • Your files in /home/$USER/backup-project/ remain intact. To resume later, re-add the cron entry.

Delete - I don't want to use this again

Remove all project resources completely. This deletes the script, all backup archives, the log file, the sample data, and the scheduled cron job.

  • Remove the cron job by opening your crontab:
crontab -e
  • Find the line that starts with 0 2 * * * and delete it. Save and exit.
  • Delete the entire project directory by running:
rm -rf /home/$USER/backup-project
  • Verify the cron job is removed by running:
crontab -l
  • You should see no entry referencing backup.sh. Verify the directory is gone by running:
ls /home/$USER/backup-project
  • You should see a "No such file or directory" message confirming full removal.

Nice Work!

Nice Work!

You just built a production-ready backup automation system from scratch using nothing but Bash and standard Linux tools. Your data is now protected by a script that compresses, rotates, logs, and runs on autopilot.

You learned how to:

  • Written a bash script that compresses directories into timestamped .tar.gz archives using tar.
  • Implemented a 7-day retention policy using find with -mtime to automatically clean up old backups.
  • Scheduled the script with cron to run daily at 2:00 AM with full timestamped logging for a complete audit trail.
  • Secret Mission: Added a pre-backup disk space check and post-backup integrity verification to catch failures before they cause data loss.

Ready to quiz yourself?