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
sudoprivileges. 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/wwwand the Nginx configuration in/etc/nginx; replace them with your own paths. - Enough free space in
/var/backupsfor at least one compressed copy of that data. Check it withdf -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-vcsskips.git,.svn,.hgand similar directories.--exclude-cachesskips directories that contain aCACHEDIR.TAGfile, 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:
| Option | Extension | Speed | Size | Notes |
|---|---|---|---|---|
-z (gzip) | .tar.gz | Medium | Medium | Available everywhere |
-I pigz | .tar.gz | Fast | Same as gzip | Parallel gzip, sudo apt install pigz |
--zstd | .tar.zst | Very fast | Smaller than gzip | Needs the zstd package (sudo apt install zstd if missing) |
-J (xz) | .tar.xz | Slow | Smallest | Good 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
Importantevery incremental archive depends on the full backup and on all earlier increments. Losing one breaks the chain. Start a new chain regularly (for example, weekly) by moving the old
.snarfile away before the next run.
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 deniedmeans you forgotsudo;Cannot stat: No such file or directorymeans 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. Checkdf -hand create it again.- Exclusions seem to be ignored: the
--excludeoptions were placed after the paths, or the pattern starts with/while the stored names do not. Compare your pattern with the output oftar -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/tarto object storage or another server with rclone or rsync. - Add database dumps made with
mysqldumporpg_dumpto 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.
