Linux command: tar
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:
| Flag | Short for | Does |
|---|---|---|
-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
Back to topThe right mental model:
taris 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.
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 tape | You 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 top3. 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:
| Era | Milestone |
|---|---|
| 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.
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.
| Flag | Long form | Program | Usual 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.
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:
| Flag | Result | Ratio | Compress | Decompress |
|---|---|---|---|---|
| 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 top6. 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.
6.2 Symbolic Links, and the Flag That Doubles Your Backup
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:
| Format | Filename limit | Use 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:
| Code | Means |
|---|---|
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.
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:
| Offset | Field | Value above | Meaning |
|---|---|---|---|
| 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 | .zip | Difference |
|---|---|---|---|
| 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.gzhas 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.gzis 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,
.zipis 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
.tarwith 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.gzcan expand to fill a disk.tar tvfshows you the stored sizes before you commit to unpacking them. -Pturns the protection off.--absolute-namestells 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
-pand--same-owneron 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.
| Need | Use | Why |
|---|---|---|
| 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.
8. Best Practices
- List before you extract.
tar tzf archive.tar.gz | headcosts 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
--excludebefore the paths. These options are positional. Modern tar warns and exits 2, but only if somebody reads the output. - Use
-Cinstead ofcd. 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 siterather than naming the full path. - Restore as root with
-pand 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
-hwill pull in. Symlinks are stored as links by default, which is nearly always right.-hfollows them, and a single link to another volume can multiply the size of a backup. - Add
-Swhen 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-ownermakes 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.sha256is 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 dfand--totalsare 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 top9. Common Mistakes
9.1 Myth Versus Reality
| Myth | Reality |
|---|---|
| "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 tzffirst, or--one-top-levelalways. - Confusing
-jand-J. Lower case is bzip2, upper case is xz. Use--bzip2and--xzin scripts, where nobody will misread them. - Backing up a directory into itself.
tar czf /var/www/backup.tar.gz /var/wwwtries 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
-uto 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
-gfile, an incremental backup silently becomes a full one, and the chain you would need to restore is broken. - Adding
-hwithout 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 reports2026-01-01: Cannot stat: No such file or directory. Use--mtime=@0if it has to live in a variable. - Piping into
tarwithoutset -o pipefail.tar czf - dir | ssh host 'cat > backup.tar.gz'reports success whenever the far end succeeds, even if tar failed halfway.
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,-Jand--zstdpipe the result through a separate program, which is why the extension has two parts. - Three modes, one of which you must pick:
-ccreate,-tlist,-xextract.-fnames the archive, and without it tar writes to standard output. - Extracting no longer needs a compression flag.
tar xfworks 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.
-Cchooses the directory on both create and extract;--strip-componentsremoves the version-numbered wrapper directory that source releases use.--excludeis 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;
-hfollows them instead, which can quietly multiply the size of a backup. -Shandles 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.gzis 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.


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












