Nandishwar Singh

Home / Learn Linux / Lesson 11

Archives and links: tar, gzip, hard links and symlinks

Pack and compress folders, and the difference between a hard link and a symlink.

Lesson
11 of 30
Time
About 40 minutes

On this page

  1. Set up
  2. Archive or compress?
  3. tar: create, list, extract
  4. Which compression?
  5. gzip on its own
  6. Inodes: what a file really is
  7. Hard links: two names, one file
  8. Symbolic links: a signpost
  9. Common mistakes
  10. Practice
  11. Cheat sheet

Set up

Make something worth packing: a small project with a config folder, a big text file and a few notes. seq makes a file of a million numbered lines, which is about 6.6 MB of very repetitive text, the kind that compresses well:

$ mkdir -p ~/arc-lab/project/{config,data,notes}
$ cd ~/arc-lab
$ cp /etc/apt/sources.list.d/ubuntu.sources project/config/
$ seq 1 1000000 > project/data/numbers.txt
$ echo "remember to rotate keys" > project/notes/todo.txt
$ du -sh project
6.6M    project

du -sh shows how much space a directory takes in total (lesson 25 covers it).

Archive or compress?

These are two separate jobs, done by separate tools:

tar can call a compressor for you, so in practice you do both at once and get a .tar.gz, also written .tgz.

tar: create, list, extract

tar takes a few letters that say what to do. You'll use these three combinations almost every time:

CommandDoes
tar -czf name.tar.gz dirCreate, gzip it, into this file
tar -tzf name.tar.gzList what's inside
tar -xzf name.tar.gzExtract it here

Add v (verbose) to see each file as it goes:

$ tar -czvf project.tar.gz project
project/
project/config/
project/config/ubuntu.sources
project/notes/
project/notes/todo.txt
project/data/
project/data/numbers.txt
$ ls -lh project.tar.gz
-rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 project.tar.gz

6.6 MB went down to about 2 MB. Look inside without extracting:

$ tar -tzf project.tar.gz
project/
project/config/
project/config/ubuntu.sources
project/notes/
project/notes/todo.txt
project/data/
project/data/numbers.txt

Extract it somewhere else with -C, which tells tar which directory to work in:

$ mkdir restore
$ tar -xzf project.tar.gz -C restore
$ tree restore
restore
└── project
    ├── config
    │   └── ubuntu.sources
    ├── data
    │   └── numbers.txt
    └── notes
        └── todo.txt

5 directories, 3 files

You can also pull out a single file by naming it exactly as tar -t lists it:

$ tar -xzf project.tar.gz -C restore project/notes/todo.txt
$ cat restore/project/notes/todo.txt
remember to rotate keys

And leave things out with --exclude:

$ tar -czf small.tar.gz --exclude="*.txt" project
$ tar -tzf small.tar.gz
project/
project/config/
project/config/ubuntu.sources
project/notes/
project/data/

A note on paths: tar stores whatever path you give it. If you archive /home/ubuntu/arc-lab/project, it strips the leading / and stores home/ubuntu/arc-lab/project/..., so extracting it recreates that whole chain of directories. cd to the parent first, or use -C, and archive the short relative name instead.

Try it

  1. Archive /etc/apt into apt-backup.tar.gz in ~/arc-lab, storing the paths as apt/... rather than etc/apt/....
  2. List the archive and count how many entries it has.
Answer
$ tar -czf apt-backup.tar.gz -C /etc apt
$ tar -tzf apt-backup.tar.gz | wc -l
28

-C /etc makes tar work from /etc, so the stored name is just apt.

Which compression?

tar supports several compressors. Swap the z for another letter:

OptionCompressorExtensionCharacter
-zgzip.tar.gzFast, works everywhere
-Jxz.tar.xzSmallest, slowest
--zstdzstd.tar.zstVery fast and nearly as small as xz
$ time tar -czf p.tar.gz project

real    0m0.220s
user    0m0.219s
sys     0m0.006s
$ time tar -cJf p.tar.xz project

real    0m1.916s
user    0m1.895s
sys     0m0.026s
$ time tar --zstd -cf p.tar.zst project

real    0m0.030s
user    0m0.027s
sys     0m0.006s
$ ls -l p.tar.*
-rw-rw-r-- 1 ubuntu ubuntu 2131841 Oct 21 11:37 p.tar.gz
-rw-rw-r-- 1 ubuntu ubuntu  327568 Oct 21 11:37 p.tar.xz
-rw-rw-r-- 1 ubuntu ubuntu  271761 Oct 21 11:37 p.tar.zst

time shows how long each one took: real is the wall-clock time. On this very repetitive file, xz squeezed 6.6 MB down to under 100 KB but took about ten times as long as gzip, and zstd was both the fastest and far smaller than gzip. Real data compresses less than a list of numbers, so your sizes will differ, but the ranking usually holds: xz smallest, zstd fastest, gzip the one every system can open. When extracting, recent versions of GNU tar detect the compression by themselves, so tar -xf file works for all three.

gzip on its own

gzip compresses single files and replaces the original. Add -k to keep it:

$ cp project/data/numbers.txt n.txt
$ gzip -k n.txt
$ ls -lh n.txt n.txt.gz
-rw-rw-r-- 1 ubuntu ubuntu 6.6M Oct 21 11:37 n.txt
-rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 n.txt.gz
$ gunzip -f n.txt.gz

gunzip reverses it (-f overwrites the existing n.txt). You'll meet .gz files mostly in /var/log, where old logs are compressed after rotation. You don't have to unpack them to read them: zcat, zless and zgrep work on .gz files directly:

$ ls /var/log/*.gz
/var/log/dmesg.1.gz  /var/log/dmesg.2.gz
$ zcat /var/log/dmesg.1.gz | head -n 3
[    0.229765] kernel: Booting Linux on physical CPU 0x0000000000 [0x610f0000]
[    0.229780] kernel: Linux version 7.0.0-28-generic (buildd@bos03-arm64-120) (aarch64-linux-gnu-gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0, GNU ld (GNU Binutils for Ubuntu) 2.46) #28-Ubuntu SMP PREEMPT_DYNAMIC Sun Jun 21 00:50:10 UTC 2026 (Ubuntu 7.0.0-28.28-generic 7.0.12)
[    0.229785] kernel: KASLR enabled

zip, for Windows people

Windows users send .zip files. Ubuntu server doesn't include the tools, so install them, then zip -r packs a directory and unzip unpacks:

$ zip -rq project.zip project
$ unzip -l project.zip
Archive:  project.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-21 11:37   project/
        0  2026-10-21 11:37   project/config/
     3000  2026-10-21 11:37   project/config/ubuntu.sources
        0  2026-10-21 11:37   project/notes/
       24  2026-10-21 11:37   project/notes/todo.txt
        0  2026-10-21 11:37   project/data/
  6888896  2026-10-21 11:37   project/data/numbers.txt
---------                     -------
  6891920                     7 files

sudo apt install -y zip unzip installs them. -q keeps zip quiet, and unzip -l lists without extracting.

Try it

  1. Make an xz-compressed archive of project/config and list it.
  2. Search the compressed log /var/log/dmesg.1.gz for the word Linux without unpacking it, showing only the first match.
Answer
$ tar -cJf config.tar.xz project/config
$ tar -tf config.tar.xz
project/config/
project/config/ubuntu.sources
$ zgrep -m 1 Linux /var/log/dmesg.1.gz
[    0.229765] kernel: Booting Linux on physical CPU 0x0000000000 [0x610f0000]

Inodes: what a file really is

To understand links, you need one more idea. A file's data and details (owner, permissions, size, times, where its blocks are on disk) live in an inode, a numbered record. The file's name is just an entry in a directory that points to an inode number. ls -i shows those numbers:

$ ls -i project/notes/todo.txt
534161 project/notes/todo.txt
$ stat -c "inode %i, %h link(s)" project/notes/todo.txt
inode 534161, 1 link(s)

Names and inodes being separate is why mv is instant inside one disk (it only changes the name), and why there are two kinds of link.

ln target newname makes a hard link: a second name pointing to the same inode. Neither name is the original. They're equally real:

$ echo "version 1" > a.txt
$ ln a.txt b.txt
$ ls -li a.txt b.txt
534180 -rw-rw-r-- 2 ubuntu ubuntu 10 Oct 21 11:37 a.txt
534180 -rw-rw-r-- 2 ubuntu ubuntu 10 Oct 21 11:37 b.txt
$ echo "version 2" >> b.txt
$ cat a.txt
version 1
version 2

Same inode number, and the link count (third column) went up to 2. A change through either name shows up in both, because there's only one file. Deleting a name only removes that name. The data stays until the last name is gone:

$ rm a.txt
$ cat b.txt
version 1
version 2
$ ls -li b.txt
534180 -rw-rw-r-- 1 ubuntu ubuntu 20 Oct 21 11:37 b.txt

Two limits: a hard link can't point to a directory, and it can't cross to a different filesystem (another disk), because inode numbers only mean something within one filesystem.

ln -s target newname makes a symbolic link (symlink): a tiny file that just contains a path. Open the link and Linux follows the path to the real file. This is the kind you saw in lesson 4 with /bin -> usr/bin:

$ ln -s project/notes/todo.txt todo-link
$ ls -l todo-link
lrwxrwxrwx 1 ubuntu ubuntu 22 Oct 21 11:37 todo-link -> project/notes/todo.txt
$ cat todo-link
remember to rotate keys

The l at the start and the -> arrow show it's a link and where it points. Symlinks can point to directories and across disks, which is why they're everywhere. But they point by name, so if the target moves or is deleted, the link breaks:

$ ln -s missing.txt broken-link
$ cat broken-link
cat: broken-link: No such file or directory
$ ls -l broken-link
lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 broken-link -> missing.txt

ls still shows a broken link, and find -xtype l finds every one of them under a directory.

A relative target, like project/notes/todo.txt, is relative to where the link is, not where you were when you made it. That's the most common reason a new symlink is broken. When in doubt, use an absolute path, or check with readlink -f, which prints the final real path:

$ readlink -f todo-link
/home/ubuntu/arc-lab/project/notes/todo.txt
$ readlink -f /bin/ls
/usr/lib/cargo/bin/coreutils/ls

The current release trick

Deployment tools keep each version of an app in its own directory and point a current symlink at the live one. Switching versions, or rolling back, is changing one link. -f replaces an existing link and -n treats an existing link to a directory as a link rather than going inside it:

$ mkdir -p releases/v1 releases/v2
$ ln -s releases/v1 current
$ ls -l current
lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v1
$ ln -sfn releases/v2 current
$ ls -l current
lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v2

Hard or soft?

Hard linkSymlink
Points toThe inode (the data)A path (a name)
DirectoriesNoYes
Across disksNoYes
If the original name is deletedStill worksBreaks
ls -l showsA normal file, link count above 1l and ->

In everyday admin work you'll make symlinks far more often. Hard links show up in backups, where tools use them so that unchanged files in each daily snapshot don't take extra space.

Try it

  1. Make a symlink called logs in ~/arc-lab pointing to /var/log, then list logs/apt through it.
  2. Make a hard link to b.txt called c.txt, then check the link count.
  3. Make a symlink to a file that doesn't exist, then find it with find.
Answer
$ ln -s /var/log logs
$ ls logs/apt
eipp.log.xz  history.log  term.log
$ ln b.txt c.txt
$ stat -c "%h links" b.txt
2 links
$ ln -s nowhere.txt dead
$ find . -xtype l
./broken-link
./dead

Common mistakes

Practice

  1. Back up the whole project directory as project-YYYY-MM-DD.tar.gz using today's date, then show its size.
Answer
$ tar -czf project-$(date +%F).tar.gz project
$ ls -lh project-*.tar.gz
-rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 project-2026-10-21.tar.gz
  1. Extract only the config directory from project.tar.gz into a new directory called only-config.
Answer
$ mkdir only-config
$ tar -xzf project.tar.gz -C only-config project/config
$ tree only-config
only-config
└── project
    └── config
        └── ubuntu.sources

3 directories, 1 file
  1. Archive project but leave out the data directory.
Answer
$ tar -czf no-data.tar.gz --exclude=project/data project
$ tar -tzf no-data.tar.gz
project/
project/config/
project/config/ubuntu.sources
project/notes/
project/notes/todo.txt
  1. Count the lines in /var/log/dmesg.1.gz without unpacking it.
Answer
$ zcat /var/log/dmesg.1.gz | wc -l
613
  1. Create releases/v3, point current at it, then roll back to v2, showing the link after each step.
Answer
$ mkdir releases/v3
$ ln -sfn releases/v3 current
$ ls -l current
lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v3
$ ln -sfn releases/v2 current
$ ls -l current
lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v2
  1. Show that b.txt and c.txt are the same file using inode numbers, then delete b.txt and show that c.txt still has the contents.
Answer
$ ls -i b.txt c.txt
534180 b.txt  534180 c.txt
$ rm b.txt
$ cat c.txt
version 1
version 2

Cheat sheet

CommandWhat it does
tar -czf out.tar.gz dirCreate a gzip archive (v to list files as it goes)
tar -tzf file.tar.gzList the contents
tar -xzf file.tar.gz -C destExtract into dest
tar -xzf file.tar.gz path/in/archiveExtract one file
--exclude="*.log"Leave things out
-J, --zstdxz or zstd instead of gzip
gzip -k file, gunzip file.gzCompress (keep original), decompress
zcat, zless, zgrepRead .gz files directly
zip -r out.zip dir, unzip file.zipZip files (install zip unzip)
ls -i, stat -c %iShow inode numbers
ln target nameHard link
ln -s target nameSymbolic link
ln -sfn target nameRepoint an existing link
readlink -f linkWhere it really ends up
find . -xtype lFind broken symlinks