tar

Archive and compress files and directories

Updated 2026-08-12

tar bundles a directory tree into a single archive file, preserving permissions, ownership, and directory structure. The name is short for "tape archive," a holdover from when the typical destination really was a tape drive, but today it's the standard way to package source trees, backups, and release artifacts on Linux.

tar itself doesn't compress anything; it just concatenates files with metadata headers. The familiar .tar.gz is a plain .tar piped through gzip afterwards. tar's -z, -j, and -J flags do exactly that pipe for you in one command, using gzip, bzip2, or xz respectively.

The three flags you'll use every time

Almost every tar invocation is one mode flag plus -v (verbose) plus -f (file):

  • -c create a new archive
  • -x extract an archive
  • -t list an archive's contents without extracting
tar -czvf site-backup.tar.gz site/    # create, gzip, verbose, to this file
tar -tzvf site-backup.tar.gz          # list, gzip, verbose
tar -xzvf site-backup.tar.gz          # extract, gzip, verbose

-f always takes the archive filename as its argument and is usually written last, right before the filename, since combined short flags (-czvf) still need their arguments in order.

Picking a compression format

gzip (-z) is fastest and universally supported; xz (-J) compresses noticeably smaller at the cost of more CPU time; bzip2 (-j) sits between them and is less common today. Unless you have a specific reason otherwise, .tar.gz is the safe default for sharing archives, and .tar.xz is worth the extra CPU when archive size matters more (release downloads, long-term backups).

Backing up incrementally

-g snapshot-file (--listed-incremental) turns a series of backups into a chain: the first run against a snapshot file is a full backup, and every later run against the same file stores only what's changed since, at the cost of needing every layer, in order, to restore.

Always verify before you trust an archive

tar -tzf archive.tar.gz > /dev/null reads through the whole archive and reports failure if anything is corrupt, without writing any files to disk. Cheap insurance before you delete the original data an archive is supposed to be backing up.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

site/ the directory every example archives - ls -lAR site

site:
total 12
drwxr-xr-x 2 user user 4096 Jul  5 15:35 css
drwxr-xr-x 2 user user 4096 Jul  5 15:35 img
-rw-r--r-- 1 user user   14 Jul  5 15:35 index.html

site/css:
total 4
-rw-r--r-- 1 user user 20 Jul  5 15:35 style.css

site/img:
total 20
-rw-r--r-- 1 user user 20480 Jul  5 15:35 logo.png

the working directory what sits alongside site/ - access.log for the compression comparison, two prebuilt archives, and empty restore targets

total 224
-rw-r--r-- 1 user user 162000 Jul  5 15:35 access.log
drwxr-xr-x 2 user user   4096 Jul  5 15:35 backups
-rw-r--r-- 1 user user  30720 Jul  5 15:35 plain.tar
drwxr-xr-x 3 user user   4096 Jul  5 15:35 restore
drwxr-xr-x 2 user user   4096 Jul  5 15:35 restore2
drwxr-xr-x 4 user user   4096 Jul  5 15:35 site
-rw-r--r-- 1 user user   9915 Jul  5 15:35 site-backup.tar.gz
-rwxr-xr-x 1 user user     15 Jul  5 15:35 suid-file
42 outputs, collapsed by default

Creating archives

The -c mode, with a compression flag and a filename.

Create a gzip-compressed archive of a directory

tar -czvf site-backup.tar.gz site/ | sort

-c create, -z gzip, -v verbose (print each file as it's added), -f the archive filename. This is the combination you'll type most.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Create an archive without compression

tar -cvf site-backup.tar site/

Drop -z for a plain, uncompressed .tar: faster to create, larger on disk. Useful as an intermediate step before compressing separately.

Create an archive whose paths don't include the parent directory

tar -czf site.tar.gz -C site .

-C site changes into site/ before archiving, so paths inside the archive start from . instead of site/. Useful when you want the archive's contents to land directly in whatever directory it's extracted into, with no wrapper folder.

Exclude files matching a pattern

tar --exclude="*.png" --sort=name -czvf site-noimg.tar.gz site/

--exclude takes a glob and skips any matching path. Repeat the flag for multiple patterns.

Show output
site/
site/css/
site/css/style.css
site/img/
site/index.html

Exclude version control metadata

tar --exclude-vcs -czf snapshot.tar.gz site/

--exclude-vcs skips .git, .svn, and other VCS directories automatically, without listing them by hand.

Exclude a list of patterns from a file

echo "*.png" > excludes.txt
tar --exclude-from=excludes.txt --sort=name -czvf site-ex.tar.gz site/

--exclude-from reads one glob per line from a file, easier to maintain than a long list of repeated --exclude flags once you have more than a couple of patterns.

Show output
site/
site/css/
site/css/style.css
site/img/
site/index.html

Archive an explicit list of files from a text file

find site -name "*.css" > filelist.txt
tar -czvf css-only.tar.gz --files-from=filelist.txt

--files-from (-T) reads one path per line from a file instead of the command line, useful when the list of files to archive comes from find, a database query, or anywhere else that isn't a simple glob.

Show output
site/css/style.css

Pipe find's output straight into tar

find site -name "*.html" -print0 | tar -czvf html-only.tar.gz --null --files-from=-

--null pairs with find -print0 so filenames containing spaces or newlines survive the trip intact instead of being split on whitespace. --files-from=- reads the (null-separated) list from stdin rather than a file.

Show output
site/index.html

Back up only files changed in the last day

touch site/index.html
find site -type f -mtime -1 -print0 | tar -czvf recent-changes.tar.gz --null --files-from=-

Combines find -mtime with the same --files-from=- pattern to archive only recently modified files, a lightweight alternative to -g's incremental snapshots for a cron job that just needs "today's changes."

Show output
site/index.html

Choosing a compression format

gzip, bzip2, and xz trade speed for ratio.

Compare compressed sizes on the same content

tar -czf app.tar.gz access.log
tar -cjf app.tar.bz2 access.log
tar -cJf app.tar.xz access.log
du -h --apparent-size app.tar.*

On a large, repetitive text log, the difference is dramatic: xz and bzip2 both find redundancy gzip misses. On already-compressed content (images, video), expect all three to end up roughly the same size as the input.

Show output
389	app.tar.bz2
743	app.tar.gz
308	app.tar.xz

Let tar pick the compressor from the filename

tar -acvf site-backup.tar.zst site/ | sort

-a (--auto-compress) reads the archive filename's extension and picks the matching compressor automatically, zstd for .tar.zst here, so you don't have to remember -z/-j/-J for less common formats. Requires the zstd package installed.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Let tar pick the decompressor from the filename on extract

tar --sort=name -acf site-backup.tar.zst site/
tar -axvf site-backup.tar.zst -C restore/

-a works the same way on extraction: it inspects the .tar.zst extension and shells out to zstd without you naming the format explicitly.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Listing archive contents

Check what's inside before you extract it.

List an archive's contents

tar -tzvf site-backup.tar.gz

-t lists without extracting; -v here shows permissions, owner, size, and date, like ls -l.

Show output

Your output will differ: the modification times are from when the sample files were created

drwxr-xr-x user/user         0 2026-07-05 15:35 site/
drwxr-xr-x user/user         0 2026-07-05 15:35 site/css/
-rw-r--r-- user/user        20 2026-07-05 15:35 site/css/style.css
drwxr-xr-x user/user         0 2026-07-05 15:35 site/img/
-rw-r--r-- user/user     20480 2026-07-05 15:35 site/img/logo.png
-rw-r--r-- user/user        14 2026-07-05 15:35 site/index.html

List contents without the long format

tar -tf site-backup.tar.gz

Just the paths, one per line, good for piping into grep or wc -l.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Extracting archives

The -x mode, and where things land.

Extract an archive into the current directory

tar -xzvf site-backup.tar.gz | sort

Recreates whatever directory structure was stored, relative to where you run the command.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Extract into a specific directory

tar -xzvf site-backup.tar.gz -C restore/

-C changes to the target directory first. The destination must already exist; tar won't create it.

Extract just one file from an archive

tar -xzf site-backup.tar.gz -C restore/ site/index.html

Add the specific in-archive path after the archive name to extract only that file, keeping its stored directory structure.

Extract without recreating the archive's top-level directory

tar -xzvf site-backup.tar.gz --strip-components=1 -C restore/ | sort

--strip-components=1 drops the first path segment of every entry during extraction, turning site/index.html into just index.html in the destination. Increase the number to drop more levels.

Show output
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Extract without overwriting files that already exist

tar -xkvf plain.tar -C restore/ | sort

-k (--keep-old-files) refuses to overwrite existing files instead of silently replacing them, safer when restoring into a directory that might already have newer versions of some files.

Show output
tar: site/css/style.css: Cannot open: File exists
tar: site/img/logo.png: Cannot open: File exists
tar: site/index.html: Cannot open: File exists
tar: Exiting with failure status due to previous errors
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Extract only files matching a wildcard pattern

tar -xzvf site-backup.tar.gz --wildcards "*.css" -C restore/

--wildcards filters at extraction time, the same way it filters a listing: only in-archive paths matching the glob get written to disk, everything else is skipped without needing a separate --exclude.

Show output
site/css/style.css

Extract a single subdirectory, keeping its path

tar -xzvf site-backup.tar.gz -C restore/ site/css | sort

Naming a directory instead of a single file extracts everything under it, keeping the stored path structure intact rather than flattening it into restore/.

Show output
site/css/
site/css/style.css

Rename paths on the fly while extracting

tar -xzvf site-backup.tar.gz --transform='s,^site,site-2025,' -C restore/ | sort

--transform runs a sed-style expression against every path as it's extracted, so site/index.html lands as site-2025/index.html on disk without repacking the archive first. -v still prints the original in-archive name, not the transformed one; check the destination directory to see the renamed result.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Updating and appending

Adding to an archive that already exists.

Append a file to an existing uncompressed archive

tar -rvf plain.tar site/index.html

-r (--append) adds files to the end of an existing .tar. This only works on uncompressed archives; you can't append to a .tar.gz directly.

Show output
site/index.html

Update a whole directory tree, not just one file

tar -uvf plain.tar site/
tar -tvf plain.tar

-u against a directory checks every file under it, not just one path. The second listing shows the catch: -u appends newer versions to the end of the archive rather than rewriting it, so a changed file can appear twice, once stale, once current. Extracting uses the later (current) copy, but the archive keeps growing every time you update it; recreate it from scratch periodically instead of updating indefinitely.

Show output

Your output will differ: the modification times are from when the sample files were created

drwxr-xr-x user/user         0 2026-07-05 15:35 site/
drwxr-xr-x user/user         0 2026-07-05 15:35 site/css/
-rw-r--r-- user/user        20 2026-07-05 15:35 site/css/style.css
drwxr-xr-x user/user         0 2026-07-05 15:35 site/img/
-rw-r--r-- user/user     20480 2026-07-05 15:35 site/img/logo.png
-rw-r--r-- user/user        14 2026-07-05 15:35 site/index.html

Verifying and troubleshooting

Trust, but check, before you delete the original.

Verify an archive is readable without extracting it

tar -tzf site-backup.tar.gz > /dev/null && echo "OK: archive is valid"

Reads through the entire archive and checks the compressed stream and headers, without writing anything to disk. Cheap insurance before deleting whatever the archive backs up.

Show output
OK: archive is valid

Detect a corrupted or truncated archive

head -c 100 site-backup.tar.gz > corrupt.tar.gz
tar -tzf corrupt.tar.gz; echo "exit=$?"

A truncated file fails partway through with a clear error instead of silently reporting an empty archive. tar exits non-zero, which a backup script should check.

Show output
gzip: stdin: unexpected end of file
tar: Child returned status 1
tar: Error is not recoverable: exiting now
exit=2

See progress on a large archive without keeping the output

tar -czvf /dev/null site/ | sort

Sending the archive itself to /dev/null while keeping -v gives you a pure progress listing, a quick way to sanity-check which files would be included before committing to a real destination.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

See the exact byte count written to an archive

tar -czf backup-totals.tar.gz --totals site/

--totals prints a summary line after the archive is written, useful in a backup script's log without parsing ls -lh output separately. The throughput figure is specific to the machine it ran on and will differ every time.

Show output

Your output will differ: the transfer rate depends on your disk

Total bytes written: 30720 (30KiB, 260MiB/s)

Verify each file immediately after writing it

tar -cWvf verify-write.tar site/ | sort

-W (--verify) re-reads every file straight back off disk after writing it and compares it to what's on disk, catching a write error at backup time instead of when you go to restore. It only works on uncompressed archives; drop -z/-j/-J or use the tar -tzf check above for a compressed one.

Show output
Verify site/
Verify site/css/
Verify site/css/style.css
Verify site/img/
Verify site/img/logo.png
Verify site/index.html
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Incremental and differential backups

-g turns a series of backups into a chain: one full backup, then only what changed.

Create a full (level 0) backup with a snapshot file

tar -czg site.snar --sort=name -f level0.tar.gz site/
tar -tzf level0.tar.gz

-g site.snar (--listed-incremental) writes a snapshot file alongside the archive recording every file's mtime and inode. The first run against a snapshot file that doesn't exist yet is always a full backup, identical in content to a plain -c.

Show output
site/
site/css/
site/img/
site/index.html
site/css/style.css
site/img/logo.png

Create an incremental backup containing only what changed

tar -czg site.snar --sort=name -f level0.tar.gz site/
sleep 1
echo "v2" > site/index.html
echo "draft" > site/new.txt
tar -czg site.snar --sort=name -f level1.tar.gz site/
tar -tzvf level1.tar.gz

Reusing the same site.snar snapshot file, tar compares the current tree against what it recorded last time and stores only the changed and new files. Every directory tar walks still shows up in the listing (each carries its own change-tracking record), but site/css/style.css and site/img/logo.png are absent: they're unchanged, so only index.html and the new new.txt were actually written.

Show output

Your output will differ: the modification times are from when the sample files were created

drwxr-xr-x user/user        32 2026-08-16 13:11 site/
drwxr-xr-x user/user        12 2026-07-05 15:35 site/css/
drwxr-xr-x user/user        11 2026-07-05 15:35 site/img/
-rw-r--r-- user/user         3 2026-08-16 13:11 site/index.html
-rw-r--r-- user/user         6 2026-08-16 13:11 site/new.txt

Restore a full backup plus its incremental layers in order

tar -czg site.snar --sort=name -f level0.tar.gz site/
sleep 1
echo "draft" > site/new.txt
tar -czg site.snar --sort=name -f level1.tar.gz site/
tar -xzf level0.tar.gz -C restore/
tar -xzf level1.tar.gz -C restore/
cat restore/site/new.txt

Extraction order matters: the level 0 archive first, then each incremental layer on top, oldest to newest. Skipping a layer or extracting out of order leaves the restored tree in whatever state the layers you did apply produced, with no warning that anything's missing.

Show output
draft

Apply file deletions recorded in an incremental backup

tar -czg site.snar --sort=name -f level0.tar.gz site/
sleep 1
echo "draft" > site/new.txt
tar -czg site.snar --sort=name -f level1.tar.gz site/
tar -xzf level0.tar.gz -C restore/
tar -xzf level1.tar.gz -C restore/
rm site/new.txt site/css/style.css
tar -czg site.snar --sort=name -f level2.tar.gz site/
tar --incremental -xzf level2.tar.gz -C restore/
find restore/site -type f | sort

A plain -x never deletes anything; it only adds or overwrites files. --incremental on extract replays the deletion records too, removing new.txt and style.css from restore/ because they were gone when level2.tar.gz was created. Point --incremental at a restore directory that holds only files from this backup chain: it deletes anything not present in the layer being applied, which will silently remove unrelated files you'd added by hand.

Show output
restore/site/img/logo.png
restore/site/index.html

Preserving ownership and permissions

What tar keeps by default, and what it strips unless you ask for it.

List entries with numeric UID/GID instead of names

tar -tzvf site-backup.tar.gz --numeric-owner

--numeric-owner shows the stored UID/GID instead of resolving them to names, useful when restoring on a machine where those numbers map to different accounts, or when the names in the archive don't exist locally at all.

Show output

Your output will differ: the modification times are from when the sample files were created

drwxr-xr-x 1000/1000         0 2026-07-05 15:35 site/
drwxr-xr-x 1000/1000         0 2026-07-05 15:35 site/css/
-rw-r--r-- 1000/1000        20 2026-07-05 15:35 site/css/style.css
drwxr-xr-x 1000/1000         0 2026-07-05 15:35 site/img/
-rw-r--r-- 1000/1000     20480 2026-07-05 15:35 site/img/logo.png
-rw-r--r-- 1000/1000        14 2026-07-05 15:35 site/index.html

Extraction strips the setuid bit by default

chmod 4755 suid-file
tar -czf suid-test.tar.gz suid-file
tar -xzf suid-test.tar.gz -C restore/
ls -l restore/suid-file

Without -p, a non-root extraction clears the setuid/setgid bits even if the archived file had them, GNU tar's safe default so restoring an archive can't hand out privileges nobody asked for.

Show output
-rwxr-xr-x 1 user user 15 Jul  5 15:35 restore/suid-file

Restore permissions exactly, including setuid, with -p

chmod 4755 suid-file
tar -czf suid-test.tar.gz suid-file
tar -xzpf suid-test.tar.gz -C restore2/
ls -l restore2/suid-file

-p (--preserve-permissions) restores the archived mode exactly, setuid bit included. Only extract with -p from an archive you trust: applied to an untrusted archive, especially as root, it can silently recreate a setuid-root binary and hand anyone who can execute it a path to privilege escalation.

Show output
-rwsr-xr-x 1 user user 15 Jul  5 15:35 restore2/suid-file

Comparing to the source directory

How much did compression actually buy you?

Stream an archive to stdout instead of a file

tar --sort=name -czf - site/ | wc -c

A single - as the filename means stdout (for -c) or stdin (for -x), the basis for streaming an archive straight over the network, e.g. tar -czf - site/ | ssh deb1 'tar -xzf - -C /srv/backups', without ever writing the archive to disk on either end.

Show output
9915

Compare a directory's size to its compressed archive

du -sh site/
tar -czf site-current.tar.gz site/
du -h --apparent-size site-current.tar.gz

du -sh sums the directory's real disk usage; compare it to the resulting archive size to judge whether the compression flag you chose is pulling its weight on this particular content.

Show output
40K	site/
9.7K	site-current.tar.gz

Streaming archives over ssh and stdin

The - filename from the previous section, put to work.

Send a directory to a remote host without a local archive file

tar -czf - site/ | ssh deb1 'tar -xzf - -C /srv/backups'

The local tar -c writes to stdout, ssh carries those bytes to the remote shell's stdin, and the remote tar -x reads them straight off its own stdin. The compressed archive never touches disk on either end. See ssh for setting up the key-based login this assumes.

Pull a remote directory into a local archive over the same connection

ssh deb1 'tar -czf - -C /srv/www site' | tar -xzf - -C backups/

The reverse direction: the remote tar -c writes to its stdout, ssh carries it back, and the local tar -x reads it from stdin. Handy for pulling a backup off a server without an intermediate file on either side.

Extract an archive piped in from another command

cat site-backup.tar.gz | tar -xzvf - -C restore/

Anything that writes a tar stream to stdout works in place of cat, most commonly curl -fsSL https://example.com/release.tar.gz | tar -xzf - -C /opt/app for installing a downloaded release without saving the tarball first.

Show output
site/
site/css/
site/css/style.css
site/img/
site/img/logo.png
site/index.html

Handling sparse files

Files with large logical holes: disk images, preallocated databases.

See how little disk a sparse file actually uses

truncate -s 100M sparse.img
dd if=/dev/urandom of=sparse.img bs=1 count=16 seek=50000000 conv=notrunc status=none
du -h --apparent-size sparse.img
du -h sparse.img

truncate creates a 100M file that's almost entirely a hole; the 16-byte dd write near the middle is the only real data in it. --apparent-size reports the logical size, plain du reports what's actually allocated on disk.

Show output
100M	sparse.img
4.0K	sparse.img

Archive it without and with sparse-file support

truncate -s 100M sparse.img
dd if=/dev/urandom of=sparse.img bs=1 count=16 seek=50000000 conv=notrunc status=none
tar -cf nosparse.tar sparse.img
du -h --apparent-size nosparse.tar
tar -cSf sparse.tar sparse.img
du -h --apparent-size sparse.tar

Without -S (--sparse), tar reads and stores every byte, holes included, so the archive balloons to the file's full logical size. -S detects the holes and stores only the real data plus their offsets. Skipping -S on a VM disk image or a preallocated database file can turn a 10K archive into a 100M one.

Show output
101M	nosparse.tar
10K	sparse.tar

By default a symlink is archived as a link, not a copy of its target.

Checking an archive against the current filesystem

Has anything changed since this backup was made?

Confirm an archive still matches the files it came from

tar -cf plain-compare.tar site/
tar -df plain-compare.tar; echo "exit=$?"

-d (--compare/--diff) reads the archive and compares each entry's metadata and content against the file on disk right now, without extracting anything. No output plus exit 0 means everything still matches.

Show output
exit=0

Detect drift between an archive and the current filesystem

tar -cf plain-compare.tar site/
echo "changed" > site/index.html
tar -df plain-compare.tar; echo "exit=$?"

Once a tracked file changes, -d lists exactly what's different about it and exits non-zero, useful for a periodic check that a "backed up" directory hasn't quietly drifted from its last backup.

Show output
site/index.html: Mod time differs
site/index.html: Size differs
exit=1