tar bundles files and directories into a single archive while keeping permissions, ownership, timestamps and symbolic links, and gzip compresses that archive. Both are installed on every Linux system, so a .tar.gz backup can be restored anywhere without extra software. In this tutorial you will archive a website and its configuration on Ubuntu 24.04, verify the archive, exclude data that should not be backed up, create incremental backups, restore files, and automate the job with a systemd timer.

Prerequisites

To follow this tutorial, you will need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges. The commands also work on Debian 12 and Rocky Linux 9, which ship the same GNU tar.
  • Some data to back up. The examples use a website in /var/www and the Nginx configuration in /etc/nginx; replace them with your own paths.
  • Enough free space in /var/backups for at least one compressed copy of that data. Check it with df -h /var/backups.

Step 1 - Creating a compressed archive

The general form is tar -czf ARCHIVE PATHS: -c creates an archive, -z compresses it with gzip and -f names the output file. Run it with sudo so that files readable only by root are included and ownership is recorded correctly:

sudo tar -czf /var/backups/www-$(date +%F).tar.gz -C / var/www etc/nginx

The -C / option changes to the root directory before adding files, so the paths are stored as var/www/... instead of /var/www/.... Relative paths are what you want: when you extract the archive, you decide where the files go. If you pass absolute paths instead, GNU tar strips the leading slash anyway and prints Removing leading '/' from member names.

Check that the archive exists and how big it is:

ls -lh /var/backups/www-*.tar.gz
-rw-r--r-- 1 root root 38M Sep 25 10:40 /var/backups/www-2026-09-25.tar.gz

The archive is readable by every user. If the data includes secrets such as application configuration files with database passwords, restrict it:

sudo chmod 600 /var/backups/www-*.tar.gz

Step 2 - Verifying the archive

Never assume that an archive is good because tar did not complain. Three quick checks catch almost every problem. First, test the gzip stream for corruption:

sudo gzip -t /var/backups/www-2026-09-25.tar.gz && echo "gzip OK"
gzip OK

Second, list the contents. -t lists instead of extracting, and -v adds permissions, owners and sizes:

sudo tar -tzvf /var/backups/www-2026-09-25.tar.gz | head -n 5
drwxr-xr-x root/root         0 2026-09-20 08:12 var/www/
drwxr-xr-x www-data/www-data 0 2026-09-24 17:03 var/www/your_domain/
-rw-r--r-- www-data/www-data 405 2026-09-20 08:12 var/www/your_domain/index.php
-rw-r----- www-data/www-data 3212 2026-09-21 11:47 var/www/your_domain/wp-config.php
...

Third, compare the archive with the live filesystem. -d (--compare) reports every file whose content, size or metadata differs; a fresh archive of data that has not changed prints nothing:

sudo tar -dzf /var/backups/www-2026-09-25.tar.gz -C /

Any output here, such as var/www/your_domain/wp-content/cache/page.html: Mod time differs, means the file changed after the backup, which is normal for caches and logs.

Finally, record a checksum so you can detect corruption after copying the archive to another machine:

sudo sh -c 'cd /var/backups && sha256sum www-2026-09-25.tar.gz > www-2026-09-25.tar.gz.sha256'
sudo sh -c 'cd /var/backups && sha256sum -c www-2026-09-25.tar.gz.sha256'

sha256sum -c looks up the file names relative to the current directory, which is why both commands run from /var/backups.

www-2026-09-25.tar.gz: OK

Step 3 - Excluding files you do not need

Caches, logs, dependency folders and version control metadata make backups bigger and slower without adding value, since they can be regenerated. Exclude them with --exclude. In current GNU tar, exclusion options only apply to the paths that follow them, so always place them before the paths:

sudo tar -czf /var/backups/www-$(date +%F).tar.gz \
    --exclude='*.log' \
    --exclude='var/www/*/wp-content/cache' \
    --exclude='node_modules' \
    --exclude-vcs \
    --exclude-caches \
    -C / var/www etc/nginx
  • --exclude='node_modules' skips any file or directory with that name at any depth.
  • --exclude-vcs skips .git, .svn, .hg and similar directories.
  • --exclude-caches skips directories that contain a CACHEDIR.TAG file, a standard marker used by many tools.

For longer lists, put one pattern per line in a file:

sudo nano /etc/backup-exclude.txt
*.log
*.tmp
*.swp
node_modules
__pycache__
var/www/*/wp-content/cache

Then reference it with --exclude-from:

sudo tar -czf /var/backups/www-$(date +%F).tar.gz --exclude-from=/etc/backup-exclude.txt -C / var/www etc/nginx

Confirm that the excluded paths are gone from the archive. This command should print nothing:

sudo tar -tzf /var/backups/www-$(date +%F).tar.gz | grep -E 'node_modules|\.log$'

Step 4 - Choosing a compression method

gzip is the most portable choice, but not the fastest or the smallest. GNU tar can use other compressors with a single option, and it detects the compression automatically when you extract:

OptionExtensionSpeedSizeNotes
-z (gzip).tar.gzMediumMediumAvailable everywhere
-I pigz.tar.gzFastSame as gzipParallel gzip, sudo apt install pigz
--zstd.tar.zstVery fastSmaller than gzipNeeds the zstd package (sudo apt install zstd if missing)
-J (xz).tar.xzSlowSmallestGood for long-term archives

On a server with several CPU cores, pigz produces a standard .tar.gz file several times faster than gzip, because it compresses on all cores:

sudo apt install pigz
sudo tar -I pigz -cf /var/backups/www-$(date +%F).tar.gz --exclude-from=/etc/backup-exclude.txt -C / var/www etc/nginx

To change the gzip compression level, pass it to the compressor. Level 1 is fastest and level 9 gives the smallest file; the default is 6:

sudo tar -I 'gzip -9' -cf /var/backups/www-$(date +%F).tar.gz -C / var/www etc/nginx

The archive produced with pigz or gzip -9 is extracted with the usual tar -xzf.

Step 5 - Making incremental backups

A full backup every night of a large, mostly static directory wastes time and space. With --listed-incremental (short form -g), tar keeps a snapshot file that records what it has already archived, and each following run only stores files that changed since the previous one.

Create the first, full backup (level 0). The snapshot file does not exist yet, so tar creates it:

sudo mkdir -p /var/backups/incremental
sudo tar -czf /var/backups/incremental/www-full.tar.gz -g /var/backups/incremental/www.snar -C / var/www

Later runs with the same snapshot file are incremental and only contain what changed:

sudo tar -czf /var/backups/incremental/www-inc-$(date +%F-%H%M).tar.gz -g /var/backups/incremental/www.snar -C / var/www

Compare the sizes to see the difference:

ls -lh /var/backups/incremental
-rw-r--r-- 1 root root  37M Sep 25 11:02 www-full.tar.gz
-rw-r--r-- 1 root root 112K Sep 25 11:10 www-inc-2026-09-25-1110.tar.gz
-rw-r--r-- 1 root root  19K Sep 25 11:10 www.snar

To restore, extract the full backup and then every incremental archive in the order they were created. Pass -g /dev/null so tar also replays deletions recorded in each increment:

sudo mkdir -p /srv/restore-inc
sudo tar -xzf /var/backups/incremental/www-full.tar.gz -g /dev/null -C /srv/restore-inc
sudo tar -xzf /var/backups/incremental/www-inc-2026-09-25-1110.tar.gz -g /dev/null -C /srv/restore-inc

Step 6 - Restoring files

Always restore into an empty staging directory first and copy what you need from there. Extracting straight over / overwrites live files with older versions:

sudo mkdir -p /srv/restore
sudo tar -xzf /var/backups/www-2026-09-25.tar.gz -C /srv/restore

When run as root, tar restores the original owners and permissions. Check it:

sudo ls -l /srv/restore/var/www/your_domain | head -n 3
total 232
-rw-r--r--  1 www-data www-data   405 Sep 20 08:12 index.php
-rw-r-----  1 www-data www-data  3212 Sep 21 11:47 wp-config.php

To restore a single file or directory, name it exactly as it appears in the listing, without a leading slash:

sudo tar -xzf /var/backups/www-2026-09-25.tar.gz -C /srv/restore var/www/your_domain/wp-config.php

To look at a file without extracting it, print it to standard output with -O:

sudo tar -xzf /var/backups/www-2026-09-25.tar.gz -O etc/nginx/sites-available/your_domain | less

Once you have checked the restored copy, move it into place, for example with sudo rsync -a /srv/restore/var/www/your_domain/ /var/www/your_domain/.

Step 7 - Automating the backup with a systemd timer

A small script turns the commands above into a nightly job with retention and checksums. Create it:

sudo nano /usr/local/bin/tar-backup.sh
#!/usr/bin/env bash
# Nightly compressed backup of selected paths with checksum and retention.
set -euo pipefail
umask 077

BACKUP_DIR="/var/backups/tar"
RETENTION_DAYS=14
SOURCES=(etc var/www home)
ARCHIVE="$BACKUP_DIR/$(hostname -s)-$(date +%Y%m%d-%H%M%S).tar.gz"

mkdir -p "$BACKUP_DIR"

# Exit code 1 means some files changed while being read; the archive is still usable.
rc=0
tar -czf "$ARCHIVE.part" --exclude-from=/etc/backup-exclude.txt \
    --exclude-vcs --exclude-caches -C / "${SOURCES[@]}" || rc=$?
if [ "$rc" -gt 1 ]; then
    echo "tar failed with exit code $rc" >&2
    rm -f -- "$ARCHIVE.part"
    exit "$rc"
fi

gzip -t < "$ARCHIVE.part"
mv -- "$ARCHIVE.part" "$ARCHIVE"
(cd "$BACKUP_DIR" && sha256sum -- "$(basename "$ARCHIVE")" > "$(basename "$ARCHIVE").sha256")

find "$BACKUP_DIR" -maxdepth 1 -name '*.tar.gz*' -mtime +"$RETENTION_DAYS" -delete

echo "Created $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"

The script writes to a .part file, tests it, and only then gives it its final name, so an interrupted run never leaves a file that looks complete. Adjust SOURCES (paths relative to /) and RETENTION_DAYS to your needs. The script expects the /etc/backup-exclude.txt file from Step 3 to exist. Make it executable and run it once:

sudo chmod 700 /usr/local/bin/tar-backup.sh
sudo /usr/local/bin/tar-backup.sh
Created /var/backups/tar/server-20260925-112530.tar.gz (41M)

Create the service unit:

sudo nano /etc/systemd/system/tar-backup.service
[Unit]
Description=Compressed tar backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/tar-backup.sh
Nice=19
IOSchedulingClass=idle

Then the timer, which runs the backup every night at 03:00:

sudo nano /etc/systemd/system/tar-backup.timer
[Unit]
Description=Nightly tar backup

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target

Enable the timer and check the next run:

sudo systemctl daemon-reload
sudo systemctl enable --now tar-backup.timer
systemctl list-timers tar-backup.timer
NEXT                        LEFT     LAST PASSED UNIT             ACTIVATES
Fri 2026-09-26 03:08:44 UTC 15h left -    -      tar-backup.timer tar-backup.service

Logs from each run are available with journalctl -u tar-backup.service.

Troubleshooting

  • tar: Exiting with failure status due to previous errors: scroll up for the real error. Permission denied means you forgot sudo; Cannot stat: No such file or directory means a path in the command does not exist.
  • file changed as we read it: a file (usually a log or database file) was being written during the backup. Exclude it, or back up databases with a proper dump tool instead of copying their files.
  • gzip: stdin: unexpected end of file: the archive is truncated, typically because the disk filled up or the copy was interrupted. Check df -h and create it again.
  • Exclusions seem to be ignored: the --exclude options were placed after the paths, or the pattern starts with / while the stored names do not. Compare your pattern with the output of tar -tzf.

Conclusion

You can now create tar.gz backups with relative paths, verify them with gzip -t, tar -d and checksums, skip unnecessary data, run incremental chains, restore single files safely, and run the whole process every night from a systemd timer. A backup stored on the same disk does not protect you from losing the server, so the natural next steps are:

  • Copy /var/backups/tar to object storage or another server with rclone or rsync.
  • Add database dumps made with mysqldump or pg_dump to the same schedule, since copying live database files with tar is not safe.
  • Test a full restore on a fresh VPS from time to time, and time how long it takes.