Skip to main content

Linux command: tar

29 August 2026

Almost everyone who uses Linux has typed tar -xzf something.tar.gz without being sure what the letters mean. That is a fair place to start, and it is where most people stop. The gap matters, because tar is the tool that packs up a website before a migration, that unpacks the software you download, and that produces the backup you will one day have to restore under pressure.

1. The Basics

tar takes many files and produces one file. That is the whole job. It walks a list of paths, and for each one it writes a small block of metadata (the name, the size, the permissions, the timestamp) followed by the file's contents, one after another, into a single stream.

1.1 The Three Things You Do With tar

Every tar command you will ever run is one of three modes, and you must pick exactly one:

FlagShort forDoes
-c create Make a new archive from files on disk
-t table of contents List what is inside, change nothing
-x extract Unpack the contents onto disk

To those you add -f (short for file), which names the archive. It is the flag people forget, and section 4.2 shows what happens when they do.

$ tar -cf site.tar site/      # create site.tar from the directory site/
$ tar -tf site.tar            # list what is in it
$ tar -xf site.tar            # extract it here

That is already enough to be useful. Everything else in this article is either compression, or control over where files land and what they look like when they arrive.

1.2 tar Does Not Compress

This surprises people, and it explains the double extension on every file you have ever downloaded. tar only concatenates. It makes the result bigger, not smaller, because it adds metadata and padding around every file.

$ echo hello > greeting.txt
$ ls -l greeting.txt
-rw-rw-r-- 1 peter peter 6 Aug 23 16:24 greeting.txt

$ tar -cf one.tar greeting.txt
$ ls -l one.tar
-rw-rw-r-- 1 peter peter 10240 Aug 23 16:24 one.tar

A six-byte file became a ten-kilobyte archive. Section 7.4 explains exactly why that number is 10240 and not something more sensible.

Compression is a separate program. When you write tar -czf site.tar.gz site/, the z tells tar to pipe its output through gzip before writing the file. That is why the name has two parts:

site.tar.gz    a tar archive, then compressed with gzip
site.tar.bz2   a tar archive, then compressed with bzip2
site.tar.xz    a tar archive, then compressed with xz
site.tgz       the same as .tar.gz, shortened for old filesystems

The right mental model: tar is a packing job, not a compression tool. It puts many things into one box. Whether you then shrink the box is a separate decision, made by a separate program.

Back to top

2. Where the Name Comes From

tar is short for tape archive.

tar  =  Tape ARchive

That is not a historical footnote. It is the single fact that explains almost every design decision in the tool, because a magnetic tape can only be read from beginning to end.

Because it was built for tapeYou see this today
A tape is a stream, not a filesystem There is no index. Finding one file means reading everything before it.
Tape drives write in fixed blocks Archives are padded to a multiple of 10240 bytes (section 7.4)
You cannot rewrite the middle of a tape You can append to an archive, but never remove from one
Tapes were the backup medium Permissions, ownership and timestamps are all preserved
The default output was the tape device Without -f, tar still writes to standard output

Nobody in this audience owns a tape drive, and the format is still shaped entirely around one. That is worth knowing, because it turns a list of quirks into a set of consequences.

The flags are also the subject of the best-known joke in Unix documentation: xkcd 1168, in which someone must extract a tar archive from memory within sixty seconds or the world ends. It is funny because the option letters really are hard to recall, and the reason is in section 3: they predate the conventions that would have made them memorable.

Back to top

3. A Short History

tar first appeared in Version 7 Unix in 1979, replacing an earlier pair of tape programs called tp and tap. It was written when a "file server" was a room with tape reels in it, and the format it introduced has outlived every piece of hardware it was designed for.

The important later work was standardisation. The original format had no version marker, so different vendors extended it in incompatible ways. POSIX fixed that twice:

EraMilestone
1979 Version 7 Unix ships tar, with 512-byte headers and octal numbers in ASCII
1988 POSIX.1-1988 defines ustar: the same layout plus a magic string and a path prefix field
1990s GNU tar adds its own extensions for long names, sparse files and incremental backups
2001 POSIX.1-2001 defines pax: extended headers that remove the length limits for good
Today GNU tar on Linux, bsdtar (libarchive) on the BSDs and macOS, both reading each other's archives

You can ask your own copy what it assumes when you give it nothing:

$ tar --version
tar (GNU tar) 1.35

$ tar --show-defaults
--format=gnu -f- -b20 --quoting-style=escape --rmt-command=/usr/sbin/rmt --rsh-command=/usr/bin/rsh

Three of those matter. --format=gnu is the archive format it writes. -f- means the default archive is standard output, which is the tape device's modern descendant. -b20 is the blocking factor, and it is the reason a six-byte file produced a ten-kilobyte archive in section 1.2.

The last two are the real museum pieces: rmt is the remote magnetic tape protocol, and rsh is the remote shell that ssh replaced thirty years ago. GNU tar still ships the ability to write to a tape drive on another machine over an unencrypted 1980s protocol, because nobody has ever had a reason to take it out.

Back to top

4. Simple Use Cases

4.1 The Three Commands You Actually Need

Start with a directory called site. These three commands cover most of daily use:

$ tar -czf site.tar.gz site/     # create, compressed with gzip
$ tar -tzf site.tar.gz           # list the contents
$ tar -xzf site.tar.gz           # extract into the current directory

Read the letters as a sentence and they stop being arbitrary: create zipped file, table of the zipped file, extract the zipped file. The f always comes last, because the archive name follows it.

GNU tar also accepts the flags with no dash at all, which is how most people learned it and why the flags look so strange in examples:

$ tar czf site.tar.gz site/     # identical to -czf, and much older syntax

This is a leftover from the original 1979 command, which had no concept of dashes. Both forms work; use whichever you find easier to remember.

4.2 What -f Really Does

The -f flag names the archive. Leave it out and tar falls back to its 1979 default, which is standard output, because that was where the tape drive lived. Modern GNU tar catches the obvious mistake:

$ tar cz site
tar: Refusing to write archive contents to terminal (missing -f option?)
tar: Error is not recoverable: exiting now

That protection only applies when the terminal is the destination. Redirect it and tar happily does what you asked, which is occasionally what you want:

$ tar cz site > site.tar.gz     # works, and is exactly what -f does
$ tar cf - site | ssh backup-server 'cat > site.tar'

The single dash in -f - means "use standard output", and it is the basis of every pipe in section 6.4.

One more error is worth recognising, because the wording is unhelpful the first time:

$ tar cf
tar: Old option 'f' requires an argument.

That means you wrote -f and then forgot to say which file.

4.3 Adding Compression

One letter selects the compressor. tar does not compress anything itself; it runs the external program and pipes through it.

FlagLong formProgramUsual extension
-z --gzip gzip .tar.gz, .tgz
-j --bzip2 bzip2 .tar.bz2
-J --xz xz .tar.xz
- --zstd zstd .tar.zst

Note that -j and -J differ only in case, and they are different compressors. That is a real source of confusion, and it is why the long forms are worth using in scripts.

You can also let tar work it out from the filename with -a (short for auto-compress):

$ tar caf backup.tar.xz site/    # picks xz because of the extension
$ tar caf backup.tar.zst site/   # picks zstd

$ file backup.tar.xz
backup.tar.xz: XZ compressed data, checksum CRC64

For extracting, none of this is necessary. Modern GNU tar detects the compression from the file itself, so tar xf handles every format above:

$ tar xf anything.tar.gz     # works
$ tar xf anything.tar.xz     # also works, no -J needed

The habit of typing xzf is harmless, but it is not required, and it fails you the day someone hands you a .tar.bz2.

The single letters give you each compressor at its default settings and nothing else. When you want a different level, or a compressor tar has no letter for, -I (long form --use-compress-program) takes an entire command:

$ tar -I 'zstd -19' -cf archive.tar.zst dir/    # maximum zstd compression
$ tar -I 'gzip -1'  -cf archive.tar.gz  dir/    # fastest gzip, for a big local copy
$ tar -I pigz       -cf archive.tar.gz  dir/    # parallel gzip, if installed

The quotes matter when the command has arguments. This is also the honest version of what -z has been doing all along: tar has always been running another program and piping through it.

4.4 Reading the Listing

Add -v (short for verbose) to -t and you get a listing that looks like ls -l, with one important difference:

$ tar -tvzf site.tar.gz
drwxrwxr-x peter/peter       0 2026-08-23 16:20 site/
drwxrwxr-x peter/peter       0 2026-08-23 16:20 site/logs/
-rw-rw-r-- peter/peter       9 2026-08-23 16:20 site/logs/error.log
-rw-rw-r-- peter/peter      14 2026-08-23 16:20 site/index.php
hrw-rw-r-- peter/peter       0 2026-08-23 16:20 site/hardlink.php link to site/index.php
drwxrwxr-x peter/peter       0 2026-08-23 16:20 site/images/
-rw-rw-r-- peter/peter  200000 2026-08-23 16:20 site/images/data.txt

The owner column is user/group rather than two separate columns, and those names are stored inside the archive. They are what the files were on the machine that created it, not what they will become on yours. Section 6.1 covers what actually happens on extraction.

Look at the fifth line. The type character is h, the size is 0, and the entry says link to site/index.php. tar noticed that two names pointed at the same file and stored the contents only once. This is one of the reasons a tar archive of a real system is often smaller than you expect, and one of the reasons it restores a system faithfully.

Back to top

5. Moderate Use Cases

5.1 Choosing a Compressor

Advice about compressors is usually given without numbers, so here are some. The corpus is 944 PHP files from a real Joomla installation, 5,836,800 bytes as a plain .tar, measured on one machine:

FlagResultRatioCompressDecompress
none 5,836,800 1.00x - -
-z gzip 848,628 6.88x 0.1s 0.02s
-j bzip2 603,118 9.68x 0.3s 0.09s
-J xz 589,636 9.90x 1.3s 0.03s
--zstd 854,309 6.83x 0.0s 0.01s

Three conclusions come out of that table, and they hold well beyond this one corpus.

xz wins on size and costs you time once. It took thirteen times longer than gzip to compress and produced a file 30% smaller. But it decompressed almost as fast as gzip. That is exactly the right trade for something you write once and download many times, which is why kernel and distribution archives are .tar.xz.

bzip2 is no longer the answer to anything. It is beaten by xz on size and by everything on speed, including on decompression, where it was the slowest in the test. It survives because old documentation recommends it.

zstd is the one to know about. It matched gzip's size while being the fastest in both directions. For a nightly backup, where compression time is real and ratio is a nice-to-have, it is usually the better default. The catch is availability: gzip is on every machine ever built, and zstd is not.

A simple rule: gzip when someone else must be able to open it, xz when you will distribute it, zstd when you are the only consumer and the job runs on a schedule.

5.2 Where the Files Land

By default tar extracts into the current directory, using the paths stored in the archive. Two options change that, and both are worth knowing before you need them.

-C (short for change directory) tells tar where to work. On extraction it means "unpack over there":

$ tar -xzf site.tar.gz -C /var/www/     # extract into /var/www, not here

On creation it means "treat this directory as the root of the archive", which is how you avoid storing a long path you do not want:

$ tar -czf fromdir.tar.gz -C site .
$ tar -tzf fromdir.tar.gz
./
./a.txt
./c.txt
./b.txt

--strip-components removes leading path elements as files come out. This is the answer to the very common annoyance of an archive that wraps everything in a version-numbered directory you do not want:

$ tar -tzf webapp-6.1.1.tar.gz | head -3
webapp-6.1.1/
webapp-6.1.1/index.php
webapp-6.1.1/administrator/

$ tar -xzf webapp-6.1.1.tar.gz -C /var/www/site --strip-components=1
$ ls /var/www/site
administrator  index.php

The files now land directly in /var/www/site instead of in /var/www/site/webapp-6.1.1. Almost every source release is packaged this way, so for deploying software this one option saves a move operation every time.

5.3 Leaving Things Out

--exclude takes a shell-style pattern and can be repeated:

$ tar -czf p.tgz --exclude='node_modules' --exclude='*.log' proj
$ tar -tzf p.tgz
proj/
proj/src/
proj/src/app.php
proj/.git/
proj/.git/config

There is also --exclude-vcs, which drops .git, .svn and their equivalents without you having to list them:

$ tar -czf p2.tgz --exclude-vcs proj

Once the list grows past two or three patterns, put it in a file instead. That file belongs in version control next to the backup script, where the next person can see what is deliberately not being backed up:

$ cat exclude.lst
cache
*.log
node_modules
.git

$ tar -czf site.tar.gz --exclude-from=exclude.lst site/

Now the trap, and it is a good one. --exclude must come before the paths. These options are positional: they only affect arguments that follow them. Put the pattern at the end, where it reads more naturally, and it does nothing:

$ tar -czf p3.tgz proj --exclude='*.log'
tar: The following options were used after non-option arguments.  These
options are positional and affect only arguments that follow them.
Please, rearrange them properly.
tar: --exclude '*.log' has no effect
tar: Exiting with failure status due to previous errors

$ echo $?
2

Modern GNU tar warns loudly and exits with an error, which is a kindness. The danger is in a backup script that sends its output to /dev/null: the archive is still written, it silently contains the files you meant to exclude, and nobody finds out. Section 6.9 is about noticing exactly this kind of thing.

5.4 Taking One File Out

You do not have to extract everything. Name the member exactly as it appears in the listing:

$ tar -tzf p.tgz                       # find the exact path first
$ tar -xzf p.tgz proj/src/app.php      # extract just that one
$ tar -xzf p.tgz -O proj/src/app.php   # print it to the screen instead

For patterns rather than exact names, add --wildcards:

$ tar -xzf p.tgz --wildcards 'proj/src/*.php'

The -O form is genuinely useful in an emergency: it lets you read one configuration file out of last night's backup without unpacking a gigabyte, and without touching what is on disk.

5.5 Absolute Paths

Give tar an absolute path and it quietly removes the leading slash:

$ tar -czf abs.tar.gz /var/www/site/index.php
tar: Removing leading `/' from member names
tar: Removing leading `/' from hard link targets

$ tar -tzf abs.tar.gz
var/www/site/index.php

This is a safety feature, and a good one. If the paths stayed absolute, extracting the archive anywhere would overwrite /var/www/site/index.php on the machine doing the extraction, no matter what directory you were standing in. By making every path relative, tar guarantees that extraction happens where you are.

The practical consequence is that you should cd to the right place, or use -C, rather than fighting the message:

$ tar -czf site.tar.gz -C /var/www site     # stores site/... not var/www/site/...
Back to top

6. Advanced Use Cases

6.1 Permissions, Ownership and the umask

"Why did the permissions change when I extracted this?" is one of the most common tar questions, and the answer is a rule almost nobody is told.

When you extract as a normal user, tar applies your umask. Here are two files, one deliberately world-writable:

$ ls -l perm/
-rw-rw-rw- open.txt
-rw------- secret.txt

$ ( umask 077; tar xf perm.tar -C pout1 )
$ ls -l pout1/
-rw------- open.txt          <-- was 666, now 600
-rw------- secret.txt

The archive stored 666 faithfully. The umask removed the group and other bits on the way out. Add -p (short for preserve permissions) and the recorded mode wins:

$ ( umask 077; tar xpf perm.tar -C pout2 )
$ ls -l pout2/
-rw-rw-rw- open.txt          <-- preserved
-rw------- secret.txt

The manual records the other half of the rule: -p and --same-owner are the default for the superuser. So the same command produces different results depending on who runs it, which is exactly why a restore done with sudo behaves differently from one done in your own account.

Ownership works the same way. The archive stores names and numbers:

$ tar -tvf perm.tar
-rw------- peter/peter  0 2026-08-23 16:24 ./secret.txt

$ tar -tvf perm.tar --numeric-owner
-rw------- 1000/1000    0 2026-08-23 16:24 ./secret.txt

As a normal user you cannot give a file away, so everything you extract belongs to you regardless of what the archive says. As root, tar restores the recorded owner, and it matches by name first. That is usually right, and occasionally very wrong: if www-data is UID 33 on the old server and UID 82 on the new one, matching by name is what you want. If the accounts do not exist on the target at all, use --numeric-owner so the numbers are restored literally.

An archive records permissions and ownership; it does not enforce them. What you actually get on extraction depends on who is running tar and what their umask is.

For moving a website between servers, this is the difference between a working restore and an afternoon of chown:

$ sudo tar -xzpf site.tar.gz -C /var/www --numeric-owner

Beyond the classic Unix bits there is a second layer of metadata that a full system backup may depend on, and tar has switches for each part of it:

$ tar --help | grep -E 'acls|xattrs|selinux'
      --acls                 Enable the POSIX ACLs support
      --no-acls              Disable the POSIX ACLs support
      --no-selinux           Disable the SELinux context support
      --no-xattrs            Disable extended attributes support

Whether these are on by default depends on how your tar was built and on the filesystem underneath, which is why the help text lists both the enabling and the disabling form. For an ordinary website that stores nothing in extended attributes it makes no difference. For a system backup it matters, and the only way to know is to archive, restore into a scratch directory, and compare. Assume nothing here.

By default tar stores a symbolic link as a symbolic link. It records where the link points and nothing else, which is almost always what you want:

$ ls -l
current -> releases/2026-08

$ tar cf sym.tar current releases
$ tar tvf sym.tar
lrwxrwxrwx peter/peter  0 2026-08-23 16:32 current -> releases/2026-08
drwxrwxr-x peter/peter  0 2026-08-23 16:32 releases/
drwxrwxr-x peter/peter  0 2026-08-23 16:32 releases/2026-08/
-rw-rw-r-- peter/peter  8 2026-08-23 16:32 releases/2026-08/index.php

That is the standard deployment layout: a current symlink pointing at a dated release directory. The archive keeps the arrangement intact, and restoring it gives you the same structure back.

-h (long form --dereference) changes this. tar follows every link and archives what is on the other end:

$ tar chf symh.tar current releases
$ tar tvf symh.tar
drwxrwxr-x peter/peter  0 2026-08-23 16:32 current/
-rw-rw-r-- peter/peter  8 2026-08-23 16:32 current/index.php
drwxrwxr-x peter/peter  0 2026-08-23 16:32 releases/
drwxrwxr-x peter/peter  0 2026-08-23 16:32 releases/2026-08/
hrw-rw-r-- peter/peter  0 2026-08-23 16:32 releases/2026-08/index.php link to current/index.php

current is now a real directory holding a real copy. In this tiny example tar was clever enough to notice the duplication and store the second copy as a hard link, but that only works because both ends were inside the same archive.

The danger is the case where they are not. A symlink pointing outside the directory you are archiving turns into a full copy of whatever it points at:

$ ls -l /var/www/site/uploads
uploads -> /mnt/storage/media        # 40 GB of images on another volume

$ tar chzf site.tar.gz /var/www/site  # -h: now archiving 40 GB, not 200 MB

This is a genuinely common way for a backup to grow overnight for no visible reason. Use -h deliberately, when the archive must be self-contained and the reader will not have the link targets, and leave it off the rest of the time.

6.3 Sparse Files

A sparse file has a large logical size but does not use that much disk, because the filesystem records the empty regions as holes rather than storing zeros. Virtual machine disk images and some database files are the usual examples:

$ truncate -s 100M sparse.img
$ du -h --apparent-size sparse.img
100M
$ du -h sparse.img
0

One hundred megabytes of file, zero bytes on disk. tar does not know about the holes unless you tell it, and the difference is not subtle:

$ tar cf plain.tar sparse.img
$ ls -l plain.tar
104867840

$ tar cSf sparse.tar sparse.img       # -S, --sparse
$ ls -l sparse.tar
10240

Without -S, tar reads the holes as the zeros the filesystem reports and writes every one of them into the archive: 104 megabytes. With -S it records the holes as holes and the archive is ten kilobytes, a factor of ten thousand.

The cost is that -S makes tar examine every file for holes, which is wasted effort on a directory of ordinary web files. Turn it on when the archive contains disk images or database files, and leave it off otherwise. Compression hides some of this (a run of zeros compresses well) but not the time spent reading and writing them.

6.4 tar as a Pipe

Because tar reads and writes streams, it composes. -f - means standard input or output, and that single convention gives you three genuinely useful commands.

Copy a directory tree, preserving everything:

$ tar cf - source/ | tar xf - -C /destination

Two tars, one pipe, no temporary file. Unlike cp -r, this preserves hard links, sparse files and permissions exactly, because both ends speak the same format.

Move a tree to another machine, without storing it anywhere:

$ tar czf - /var/www/site | ssh user@newserver 'tar xzf - -C /var/www'

This is worth understanding properly, because it solves a real problem: you need no disk space for an intermediate archive on either side. The compression happens on the sending machine, so it also saves bandwidth. For a one-off migration of a website it is often faster and simpler than rsync, which is the better tool only when you will repeat the transfer.

Look inside an archive without unpacking it:

$ tar xzf backup.tar.gz -O var/www/site/configuration.php | grep password

All three follow from the same 1979 decision that the default output is a device rather than a file.

6.5 Formats, and the 100-Character Wall

The original 1979 header reserved exactly 100 bytes for the filename. That was generous for a system where paths were short, and it became a problem the moment directories got deeper. Three formats now exist, and GNU tar writes the third by default:

FormatFilename limitUse it when
ustar 100 chars, or 256 if it can be split at a / Maximum portability to ancient systems
gnu No practical limit The default; fine everywhere Linux is involved
pax No limit, plus sub-second timestamps and extended attributes The POSIX standard; the safest long-term choice

The limit is not theoretical. A 124-character filename is refused outright by ustar:

$ tar cf u.tar --format=ustar aaaa...124-characters...txt
tar: aaaa...: file name is too long (cannot be split); not dumped
tar: Exiting with failure status due to previous errors
$ echo $?
2

$ tar cf g.tar aaaa...124-characters...txt     # gnu, the default
$ echo $?
0

In practice you will never choose ustar. The choice worth making is pax for anything you intend to keep for years, because it is the actual standard and it stores timestamps at full precision rather than rounding to the second.

$ tar --format=pax -czf archive.tar.gz site/

6.6 Appending, and Why Never to a .tar.gz

Because an archive is a stream with an end marker, you can add to it by overwriting the marker. -r (short for append) does that:

$ tar cf plain.tar site/index.php
$ tar rf plain.tar site/logs/error.log
$ tar tf plain.tar
site/index.php
site/logs/error.log

-u (short for update) is the same thing but only adds files that are newer than the copy already in the archive. Neither removes anything: an "updated" file is appended, and the old version stays where it was. Repeated updates make an archive that grows forever and holds several versions of the same path, with tar extracting the last one it reads.

On a compressed archive, none of this works at all:

$ tar rf site.tar.gz site/logs
tar: Cannot update compressed archives
tar: Error is not recoverable: exiting now
$ echo $?
2

The reason is structural. gzip produces one continuous stream, so there is no way to find the end marker without decompressing everything, and no way to write past it without recompressing. If you need to add to a compressed archive, you decompress, append, and recompress. In practice you create a new archive instead.

There is also no delete. tar can never remove a member from an archive, on tape or on disk, for the same reason.

6.7 Incremental Backups

GNU tar can take a full backup and then archive only what changed. The state lives in a snapshot file given with -g:

$ tar czf full.tgz -g snapshot.snar data
$ tar tzf full.tgz
data/
data/one.txt

$ touch data/two.txt

$ tar czf incr.tgz -g snapshot.snar data
$ tar tzf incr.tgz
data/
data/two.txt

The second archive contains only the new file. The snapshot file is what makes it work, and it is also the weak point: lose it and tar has no idea what it already backed up, so the next run silently becomes a full backup again. Copy it alongside the archives.

Restoring means extracting the full archive and then each increment in order, which is exactly as fragile as it sounds. Incremental tar is worth knowing about; for anything important, a tool that tracks its own history, such as restic or borg, will hurt less.

6.8 Reproducible Archives

Pack the same files twice and you get two different archives. Nothing about the contents changed; the metadata did:

$ tar cf r1.tar d ; touch d/a.txt ; tar cf r2.tar d
$ sha256sum r1.tar r2.tar
4871b10e9a0df295ddd3da758d774ca9527b2381cb16ced15149230f9d0fe40f  r1.tar
c3e4021cbbf022087435c0c98e52fc37adebcbb1ca17537c0d714af92e404c25  r2.tar

Timestamps, ownership and the order in which the filesystem returned the directory entries all end up in the archive. Pin every one of them and the result becomes deterministic:

$ tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner -cf p1.tar d
$ touch d/a.txt d/b.txt
$ tar --sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner -cf p2.tar d
$ cmp p1.tar p2.tar && echo IDENTICAL
IDENTICAL

The mtimes were deliberately changed between the two runs and the archives are still byte for byte the same. That turns "has anything actually changed since the last release?" into a checksum comparison instead of an argument.

Two details about --mtime. Write UTC in front of the date, or the stamp follows the timezone of whichever machine built the archive and two identical builds in different countries disagree. And keep the quotes: because the value contains a space, putting these flags in an unquoted shell variable splits them and tar reports 2026-01-01: Cannot stat: No such file or directory. If you want them in a variable, use --mtime=@0, which has no space in it.

One detail is worth knowing because it is the opposite of what people expect: tar czf is reproducible, but tar cf followed by gzip is not. gzip records the original filename and modification time in its header, and when it reads from a pipe there is neither to record:

$ hexdump -C reproducible.tar.gz | head -1
1f 8b 08 00 00 00 00 00  00 03 ...        <-- flags 00, timestamp all zeros

$ gzip -k plain.tar ; hexdump -C plain.tar.gz | head -1
1f 8b 08 08 a3 07 8b 6a  00 03 74 2e 74 61 72 00   |.......j..t.tar.|
                                                    <-- flag 08, a timestamp,
                                                        and the filename

Compressing a file on disk stores its name and the time; compressing a stream cannot. If you do build the archive in two steps, use gzip -n to leave both out.

6.9 Checking That the Archive Is Good

tar has three exit codes, and they are worth testing for in a script:

CodeMeans
0 Everything worked
1 Some files differ (only from -d, or a file changed while being read)
2 A fatal error. The archive may exist and be incomplete.

-d (short for diff, also --compare) checks an archive against the filesystem:

$ tar df cmp.tar
site/index.php: Mod time differs
site/index.php: Size differs
$ echo $?
1

Run immediately after creating an archive, this proves the archive matches what is on disk. It is not a substitute for a restore test, but it costs one command.

--totals prints what was written, which is a cheap sanity check in a backup script:

$ tar cf /dev/null --totals site
Total bytes written: 10240 (10KiB, 89MiB/s)

The value of that number is not the number itself but its stability. A nightly backup that has written about the same amount for months and suddenly writes a tenth of it is telling you something, and it is the only warning you are going to get.

Neither of those tells you whether the archive is still intact a month later on the disk you copied it to. For that, write a checksum next to it at the moment you create it:

$ sha256sum backup.tar.gz > backup.tar.gz.sha256

# before you rely on it, wherever it has ended up
$ sha256sum -c backup.tar.gz.sha256
backup.tar.gz: OK

A checksum answers exactly one question: are these the same bytes that were written? It says nothing about whether the right files went in. The three checks stack, cheapest first: tar tf archive > /dev/null proves the structure reads, the checksum proves the bytes survived, and an actual extraction into a scratch directory proves the backup is a backup. Only the last one is evidence.

Back to top

7. Something Most Users Do Not Know

7.1 Inside the 512-Byte Header

A tar archive is simple enough to read by eye, and doing it once makes everything else about the format obvious. Here is the first 512 bytes of an archive containing one file called greeting.txt holding the word "hello":

$ tar cf one.tar greeting.txt
$ head -c 512 one.tar | hexdump -C
00000000  67 72 65 65 74 69 6e 67  2e 74 78 74 00 00 00 00  |greeting.txt....|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000060  00 00 00 00 30 30 30 30  36 36 34 00 30 30 30 31  |....0000664.0001|
00000070  37 35 30 00 30 30 30 31  37 35 30 00 30 30 30 30  |750.0001750.0000|
00000080  30 30 30 30 30 30 36 00  31 35 32 34 32 36 30 31  |0000006.15242601|
00000090  32 35 33 00 30 31 32 32  34 37 00 20 30 00 00 00  |253.012247. 0...|
00000100  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  00 75 73 74 61 72 20 20  00 70 65 74 65 72 00 00  |.ustar  .peter..|

Every field is at a fixed offset, and every number is written as octal digits in ASCII:

OffsetFieldValue aboveMeaning
0 name (100 bytes) greeting.txt Null-padded. This is the 100-character wall from 6.5.
100 mode 0000664 Permissions, in octal
108 uid 0001750 Octal 1750 = 1000
124 size 00000000006 Six bytes, in octal
136 mtime 15242601253 Octal for 1787495083, a Unix timestamp
148 checksum 012247 The sum of the header bytes
156 typeflag 0 Regular file. 5 is a directory, 1 a hard link, 2 a symlink.
257 magic ustar The marker POSIX added in 1988
265 uname peter The owner's name, which is why 6.1 can match by name

Why octal ASCII rather than binary numbers? Because in 1979 there was no agreement on byte order or integer size between machines, and a string of digits means the same thing everywhere. It is wasteful and it is the reason a tar archive written on a PDP-11 can still be read today.

The file's contents begin at offset 512, padded out to another 512-byte boundary:

$ dd if=one.tar bs=512 skip=1 count=1 | head -c 20 | cat -A
hello$
^@^@^@^@^@^@^@^@^@^@^@^@^@^@

And the archive ends with two full blocks of zeros. That end marker is what -r overwrites when it appends, and its absence is what makes a truncated archive detectable.

7.2 Why .tar.gz Beats .zip on Size and Loses on Everything Else

This is the most useful thing in this article for anyone who works with websites, because Linux hands you .tar.gz and web applications hand you .zip, and the difference is not cosmetic.

A zip file compresses each file separately and keeps an index at the end. A .tar.gz concatenates everything first and then compresses the whole stream as one unit. That is called a solid archive, and it has one advantage and one serious disadvantage.

Here is the same real 125 MB website packed both ways:

Operation.tar.gz.zipDifference
Archive size 27 MB 34 MB tar.gz is 21% smaller
List every entry 0.397s 0.033s zip is 12x faster
Extract one file near the end 0.393s 0.003s zip is 130x faster

The size win comes from compressing across file boundaries: a thousand PHP files share a great deal of text, and gzip can only exploit that if it sees them as one stream.

The cost is that there is no index and no random access. To list a .tar.gz, tar must decompress the entire file. To pull out the last member, it must decompress the entire file. The time is the same in both cases, which the numbers above show clearly: 0.397s to list everything, 0.393s to extract one small file. The work is identical, because the work is "decompress 27 MB".

A .tar.gz has no table of contents. Every question you ask it is answered by decompressing the whole thing, which is why listing an archive and pulling one small file out of it take the same time.

This has direct consequences:

  • For a backup you restore whole, .tar.gz is the better choice: smaller, and you were going to read all of it anyway.
  • For an installer or extension package that software must inspect, list, or partially unpack, .zip is the better choice. This is why Joomla, WordPress and browser extensions all use zip, and it is not an accident or a Windows habit.
  • A plain .tar with no compression does support seeking, which is why some tools store an uncompressed tar inside another container.

7.3 The Tarbomb

An archive does not have to contain a top-level directory. When it does not, extracting it scatters files into whatever directory you happen to be standing in. That is a tarbomb, and cleaning one up by hand is miserable because you cannot tell which files were already there.

$ ls land/
existing.txt

$ tar xzf ../bomb.tar.gz
$ ls land/
a.txt  b.txt  c.txt  existing.txt

Two habits prevent this permanently. The first is to look before you extract, which takes one second:

$ tar tzf bomb.tar.gz
a.txt
b.txt
c.txt

No common prefix on those lines means the archive will unpack into the current directory. The second habit is to let tar make a directory for you:

$ tar xzf bomb.tar.gz --one-top-level
$ ls -F
bomb/

--one-top-level creates a directory named after the archive, minus its extensions, and puts everything inside. Used unconditionally it is harmless, because an archive that already has a single top-level directory keeps it.

7.4 Why a Six-Byte File Makes a 10 KB Archive

Section 1.2 left a number unexplained. The answer is in --show-defaults: -b20.

tar writes in records of 512 bytes, and it groups them into blocks of 20 records before writing, because that is how tape drives worked. 20 times 512 is 10240, and every archive is padded up to a multiple of that number.

$ tar cf one.tar greeting.txt      ; ls -l one.tar     # 10240
$ tar cf b1.tar -b1 greeting.txt   ; ls -l b1.tar      # 2048

With -b1 the same archive is 2048 bytes: one 512-byte header, one 512-byte data record, and 1024 bytes of end marker. The padding is real, and it is why a directory of thousands of tiny files produces a surprisingly large .tar.

It also disappears completely the moment you compress, because a long run of zeros is the easiest thing in the world for gzip to shrink. There is no reason to tune the blocking factor on a modern system; there is a good reason to understand why the number is 10240 when you see it.

7.5 Extracting an Archive You Do Not Trust

Extraction writes to your filesystem, so an archive from a stranger deserves the same suspicion as any other untrusted input. The usual advice lists three dangers: paths containing .., absolute paths, and symlinks that redirect writes outside the target directory. It is worth knowing that on modern GNU tar all three are already blocked.

Relative escapes are stripped when the archive is made:

$ tar cf trav.tar ../../evil/sub/evil.txt
tar: Removing leading `../../' from member names

Absolute paths are stripped twice, once on create and again on extract, so even an archive built to hold them lands relative to where you are standing:

$ tar tf abs.tar          # this archive really does contain an absolute path
/etc/hostname

$ cd /tmp/sandbox && tar xf abs.tar
tar: Removing leading `/' from member names
$ find .
./etc/hostname            <-- inside the sandbox, not the real /etc

The symlink redirect is refused. This is the interesting one. The attack is an archive holding a symlink that points somewhere outside, followed by a member written through it. tar creates the link, then declines to follow it:

$ tar tvf attack.tar
lrwxrwxrwx  0  link -> /tmp/scratch/OUTSIDE
-rw-rw-r-- 24  link/payload

$ tar xf attack.tar
tar: link/payload: Cannot open: Not a directory
tar: Exiting with failure status due to previous errors

$ ls /tmp/scratch/OUTSIDE
                          <-- empty. Nothing escaped.

So the frightening version of this warning is out of date. What remains is smaller but real, and worth the two habits below:

  • Overwriting is not prevented. Nothing stops an archive from replacing files that already exist in the directory you extract into. That is the tarbomb of section 7.3, and it is still the most likely way to lose work.
  • Decompression bombs are not prevented. A few kilobytes of .tar.gz can expand to fill a disk. tar tvf shows you the stored sizes before you commit to unpacking them.
  • -P turns the protection off. --absolute-names tells tar to keep leading slashes on both create and extract. There is no reason to use it on an archive you did not make.
  • Extracting as root removes the safety net that ordinary file permissions were providing, and turns -p and --same-owner on by default.

The practice that covers all of it costs three commands:

$ tar tvf unknown.tar.gz | less     # look first: paths, sizes, anything odd
$ mkdir unpack && cd unpack         # a new, empty directory
$ tar xf ../unknown.tar.gz          # as yourself, never as root

7.6 Knowing Where tar Stops

Part of expertise is knowing when a tool is the wrong one.

NeedUseWhy
Repeat a transfer efficiently rsync tar always sends everything; rsync sends differences
Backups with history and deduplication restic, borg tar has no index, no dedup, and fragile increments
An archive other software must open zip Random access, and it is what web applications expect
Remove a file from an archive Recreate it tar cannot delete members, by design
A consistent copy of a live database mysqldump and friends tar copies files that are changing underneath it

That last row deserves emphasis, because it is the most expensive mistake on the list. Running tar over a directory that is being written to produces an archive of a moment that never existed, and tar will tell you only if it happens to notice, with a warning that exits 1 and is usually discarded. A website's files are usually safe enough; its database is not.

Back to top

8. Best Practices

  • List before you extract. tar tzf archive.tar.gz | head costs a second and tells you whether the archive has a top-level directory, where the files will land, and whether it is what you think it is.
  • Or just use --one-top-level. Unconditionally. It is harmless on well-made archives and it makes tarbombs impossible.
  • Put --exclude before the paths. These options are positional. Modern tar warns and exits 2, but only if somebody reads the output.
  • Use -C instead of cd. It works on both create and extract, it is explicit, and it keeps a backup script from depending on the directory it was started in.
  • Do not fight the leading-slash message. Absolute paths are stripped for your safety. Create with -C /var/www site rather than naming the full path.
  • Restore as root with -p and think about --numeric-owner. Permissions and ownership only survive if the person extracting has the right to set them, and account numbers rarely match between servers.
  • Know what -h will pull in. Symlinks are stored as links by default, which is nearly always right. -h follows them, and a single link to another volume can multiply the size of a backup.
  • Add -S when the archive contains disk images or database files. Without it a 100 MB sparse file becomes 100 MB of zeros in the archive instead of ten kilobytes.
  • Pin the metadata for anything you release. --sort=name --mtime='UTC ...' --owner=0 --group=0 --numeric-owner makes the archive byte-identical between builds, so a checksum answers "did anything change?".
  • Write a checksum next to the archive when you create it. sha256sum backup.tar.gz > backup.tar.gz.sha256 is the only way to know later that the bytes survived the copy.
  • Unpack anything you did not create into a new empty directory, as yourself. Modern tar blocks the path-escape tricks, but nothing stops an archive from overwriting what is already in the directory you chose.
  • Never append to a compressed archive. It cannot work. Create a new one.
  • Choose the compressor deliberately. gzip when someone else must open it, xz when it will be downloaded many times, zstd when it runs nightly and you are the only consumer.
  • Check the exit code in scripts. 0 is fine, 1 means something changed underneath you, 2 means the archive may be incomplete. A backup script that ignores this reports success either way.
  • Dump the database separately. tar over a live database directory produces a file that looks like a backup and is not one.
  • Test the restore, not the archive. tar df and --totals are cheap sanity checks; neither proves you can rebuild the site. Only extracting somewhere and looking does that.
$ man 1 tar               # the full manual, options grouped by mode
$ tar --help              # a much shorter summary
$ tar --show-defaults     # what your build assumes when you say nothing
$ info tar                # the GNU manual, far more detailed than the man page
Back to top

9. Common Mistakes

9.1 Myth Versus Reality

MythReality
"tar compresses files." It never has. It concatenates, and makes the result larger. gzip, xz and friends do the compressing.
"You must give -z to extract a .tar.gz." Modern GNU tar detects the compression itself. tar xf handles gz, bz2, xz and zst.
".tar.gz and .zip are the same thing with different names." zip has an index and compresses per file; tar.gz is one solid stream. That is a 130x difference in extracting one file.
"--exclude works wherever you put it." It is positional. After the paths it does nothing, and tar exits 2 saying so.
"tar preserves permissions." It records them. Whether they are restored depends on -p and on who is extracting; a normal user gets their umask applied.
"I can add a file to my backup.tar.gz." You cannot. Compressed archives cannot be appended to, and nothing can ever be deleted from a tar archive.
"bzip2 gives the best compression." xz beats it on ratio and on decompression speed. bzip2 is a recommendation that outlived its reason.
"An empty tar archive is empty." The minimum is 10240 bytes, because of the blocking factor inherited from tape drives.
"A tar archive can write anywhere on the filesystem." Not on modern GNU tar. Leading / and ../ are stripped, and a symlink redirect is refused with "Cannot open: Not a directory". -P turns that off.
"A sparse disk image archives as small as it is on disk." Only with -S. Without it, a 100 MB sparse file writes 104,867,840 bytes of zeros into the archive.
"Packing the same files twice gives the same archive." It does not. Timestamps, ownership and directory order all differ. It takes five flags to make tar deterministic.
"tar 0 means the backup is good." It means tar finished. It says nothing about whether the archive can rebuild a working system.

9.2 Other Traps to Avoid

  • Extracting without looking. A tarbomb scatters files into your current directory, mixed with what was already there. tar tzf first, or --one-top-level always.
  • Confusing -j and -J. Lower case is bzip2, upper case is xz. Use --bzip2 and --xz in scripts, where nobody will misread them.
  • Backing up a directory into itself. tar czf /var/www/backup.tar.gz /var/www tries to archive the file it is writing. tar usually notices and skips it, but the result is not what you meant. Write the archive somewhere else.
  • Archiving a live database directory. The files change while tar reads them, and the archive contains a state that never existed. Dump the database first, then archive the dump.
  • Assuming ownership will be restored. As a normal user it never is; everything becomes yours. As root it is restored by name, which is right until the target machine does not have those accounts.
  • Trusting -u to keep an archive current. It appends new versions and never removes old ones, so the archive grows and holds several copies of the same path.
  • Losing the snapshot file. Without the -g file, an incremental backup silently becomes a full one, and the chain you would need to restore is broken.
  • Adding -h without checking what the links point at. One symlink to a media volume turns a 200 MB site backup into a 40 GB one, and the only symptom is that the backup got slow.
  • Putting the reproducible flags in an unquoted shell variable. --mtime='UTC 2026-01-01' contains a space, so the shell splits it and tar reports 2026-01-01: Cannot stat: No such file or directory. Use --mtime=@0 if it has to live in a variable.
  • Piping into tar without set -o pipefail. tar czf - dir | ssh host 'cat > backup.tar.gz' reports success whenever the far end succeeds, even if tar failed halfway.
Back to top

10. Summary

tar is a program built for hardware nobody has used in decades, and it is still the right answer to "put this directory in one file" on every Unix system in the world.

  • tar means tape archive. Almost every quirk in it follows from a tape being a stream you can only read start to finish.
  • It concatenates; it does not compress. -z, -j, -J and --zstd pipe the result through a separate program, which is why the extension has two parts.
  • Three modes, one of which you must pick: -c create, -t list, -x extract. -f names the archive, and without it tar writes to standard output.
  • Extracting no longer needs a compression flag. tar xf works out the format itself.
  • On real data, xz gave the best size and gzip the best compatibility, while zstd was fastest in both directions. bzip2 was beaten on every measure.
  • -C chooses the directory on both create and extract; --strip-components removes the version-numbered wrapper directory that source releases use.
  • --exclude is positional and must come before the paths, or it silently does nothing and tar exits 2.
  • Absolute paths are stripped on purpose, so extraction always happens where you are.
  • Permissions are recorded but filtered by your umask unless you pass -p, which is the default only for root. Ownership is matched by name.
  • tar composes as a pipe: two tars copy a tree, and one over ssh moves a site without a temporary file anywhere.
  • Symlinks are stored as links; -h follows them instead, which can quietly multiply the size of a backup.
  • -S handles sparse files. Without it a 100 MB image with nothing in it writes 100 MB of zeros.
  • Archives are not reproducible by default, but five flags make them byte-identical between builds.
  • Modern GNU tar already blocks the classic extraction attacks. What it does not block is overwriting what is already in the directory you chose.
  • A .tar.gz is a solid archive: smaller than a zip, but with no index, so extracting one file costs as much as extracting all of them.
  • Look before you extract, or pass --one-top-level, and a tarbomb can never catch you.

This is the quick reference worth keeping:

tar czf out.tar.gz dir/        create, gzip
tar caf out.tar.xz dir/        create, compressor chosen by extension
tar tzf out.tar.gz             list (ALWAYS do this before extracting)
tar tvzf out.tar.gz            list with sizes, owners and modes
tar xf out.tar.gz              extract (format detected automatically)
tar xf out.tar.gz -C /target   extract somewhere else
tar xf out.tar.gz --one-top-level    never be tarbombed again

-c create   -t list   -x extract   -f FILE   -v verbose
-h follow symlinks (CHECK first)   -S sparse files (disk images)
-z gzip     -j bzip2  -J xz        --zstd    -a by extension
-C DIR      change directory (create AND extract)
-p          keep permissions exactly (default only for root)

--strip-components=1           drop the wrapper directory
--exclude='*.log'              BEFORE the paths, or it does nothing
--exclude-vcs                  drop .git, .svn and friends
--numeric-owner                restore UIDs, not names, across servers
--one-top-level                unpack into a directory named after the archive
--format=pax                   the standard format, for long-term archives
--exclude-from=list.txt        patterns from a file, kept in version control
-I 'zstd -19'                  any compressor, with its own options
-g snapshot.snar               incremental backup state

--sort=name --mtime='UTC 2026-01-01' --owner=0 --group=0 --numeric-owner
                               byte-identical archives between builds

tar cf - src | tar xf - -C dst          copy a tree, keeping hard links
tar czf - dir | ssh host 'tar xzf -'    move a site, no temp file
tar xzf a.tgz -O path/to/file           read one file out, without unpacking
tar df archive.tar                      compare archive against disk
tar tf archive.tar >/dev/null           cheap "is it corrupt" check
sha256sum a.tgz > a.tgz.sha256          prove the bytes survive the copy
tar tvf unknown.tgz | less              ALWAYS, before unpacking a stranger's

exit 0 ok | 1 files differ or changed while reading | 2 fatal

Backups and migrations are the two jobs where tar quietly does most of the work, and both are only as good as the restore nobody has tested. If you want the archives your server produces to be ones you can actually rebuild a website from, that is exactly the kind of quiet work I enjoy helping with.

Back to top
Linux command: tar
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.