Linux has two kinds of link and they are not variations on the same idea — they work at different levels of the filesystem. Understanding the difference takes one paragraph about inodes, and after that everything else follows.

Filenames are not files

A file’s actual data and metadata live in an inode, identified by a number. A directory entry is just a name pointing at an inode number. The name is not the file; it is a label attached to it.

Once you accept that, both link types make sense:

  • A hard link is another name pointing at the same inode. Two names, one file, equal standing — neither is the “original”.
  • A symbolic link is a small file of its own whose contents are a path. Following it means resolving that path, whatever happens to be there.
$ ls -li
1310721 -rw-r--r--  2 kevin kevin   42 Aug 23 10:14 notes.txt
1310721 -rw-r--r--  2 kevin kevin   42 Aug 23 10:14 hardlink.txt
1310745 lrwxrwxrwx  1 kevin kevin    9 Aug 23 10:15 symlink.txt -> notes.txt

-i shows inode numbers. Note that notes.txt and hardlink.txt share inode 1310721 and both show a link count of 2. The symlink has its own inode, a size of 9 bytes (the length of the path it stores), and an l file type.

Creating them

ln target linkname          # hard link
ln -s target linkname       # symbolic link

The argument order is the thing that trips everyone up. It is the same order as cp: what exists first, what you are creating second. Getting it backwards produces a link named after your target pointing at nothing.

ln -s /opt/app/releases/v2.1 /opt/app/current    # correct
ln -s /opt/app/current /opt/app/releases/v2.1    # backwards, and now confusing

Omit the second argument and the link is created in the current directory using the target’s basename — handy once you trust yourself.

Which to use

Hard linkSymbolic link
Points toAn inodeA path, as text
Across filesystemsNoYes
To a directoryNo (except . and ..)Yes
Survives the target being renamedYesNo — becomes broken
Survives the target being deletedYes — data persistsNo — becomes broken
Can point at nothingNoYes (a dangling link)
Visible in ls -lOnly as a link countYes, with ->
Extra disk usedNoneOne small inode

In practice, use symbolic links. They can cross filesystems, work on directories, and are visible — anyone reading ls -l can see what is going on. Hard links are for specific jobs: deduplicating identical files, and backup schemes that snapshot by linking unchanged files rather than copying them.

The relative path trap

A symlink stores exactly the text you typed. If that text is a relative path, it is resolved relative to the symlink’s own directory, not to where you were standing when you created it.

$ cd /home/kevin
$ ln -s notes.txt /tmp/mylink
$ ls -l /tmp/mylink
lrwxrwxrwx 1 kevin kevin 9 Aug 23 10:20 /tmp/mylink -> notes.txt
$ cat /tmp/mylink
cat: /tmp/mylink: No such file or directory

The link points at /tmp/notes.txt, which does not exist. Nothing warned you, because ln -s does not check that the target exists — that is a feature, since it lets you create a link before its target.

Use absolute paths unless you specifically want a relative one, or let ln work it out for you:

ln -s /home/kevin/notes.txt /tmp/mylink    # absolute: unambiguous
ln -sr /home/kevin/notes.txt /tmp/mylink   # -r computes a correct relative path

Relative links are the right choice when the whole tree might move together — inside a project directory, or a structure that gets copied elsewhere.

Updating a symlink

Pointing an existing link somewhere new has one sharp edge that has broken a lot of deployments.

ln -sf /opt/app/releases/v2.2 /opt/app/current

If current is an existing symlink to a directory, -f follows it and creates the new link inside that directory — you end up with /opt/app/releases/v2.1/v2.2 and the deployment silently pointing at the old release.

The fix is -n, which treats the destination as a file rather than descending into it:

ln -sfn /opt/app/releases/v2.2 /opt/app/current

ln -sfn is the invocation to memorise for anything that repoints a directory symlink. For a zero-downtime swap, create the new link under a temporary name and mv -T it into place — a rename is atomic, whereas delete-then-create leaves a window with no link at all.

Inspecting and finding links

readlink mylink              # what the link literally contains
readlink -f mylink           # the fully resolved final destination
stat notes.txt               # includes the inode and link count
ls -li                       # inode numbers alongside the listing

find /path -type l                    # every symlink
find /path -xtype l                   # broken symlinks only
find / -samefile /path/to/file        # all hard links to one file
find /path -type l -exec ls -l {} +   # symlinks with their targets

find -xtype l is worth running occasionally on anything you maintain — broken symlinks accumulate quietly and only announce themselves when something tries to follow one. See find for the rest.

Where you will actually meet them

  • Deployments. current -> releases/v2.2, repointed atomically. Rolling back is one ln -sfn.
  • Dotfiles. Keep them in a git repository and symlink them into $HOME, so your configuration is version-controlled.
  • Alternatives. /usr/bin/python3 and /usr/bin/editor are symlinks the system manages for you.
  • Shared libraries. libfoo.so -> libfoo.so.1.2.3, so the linker finds the current version.
  • Backups. Snapshot tools hard-link unchanged files between runs, so ten daily snapshots of mostly-identical data cost roughly one copy.
  • systemd. systemctl enable works by creating a symlink in a .wants directory — see systemctl.

Gotchas

The trailing slash

rm mylink removes the link. rm mylink/ is an error, and some commands treat mylink/ as “the directory it points to” — which for rsync --delete is the difference between replacing a link and emptying a directory. Leave the slash off when you mean the link itself.

Copying links

cp follows symlinks by default and copies the contents. cp -a (or -P) preserves them as links. rsync -a preserves symlinks but not hard-link relationships unless you add -H. tar stores symlinks as symlinks unless you pass -h.

Each of these defaults is defensible and none of them agree, so check before archiving anything where it matters.

Permissions on a symlink are meaningless

A symlink always shows lrwxrwxrwx. Access is governed entirely by the target’s permissions — see file permissions. chmod on a symlink changes the target, not the link.

Hard links and disk usage

Two hard-linked names occupy one file’s worth of space, but tools disagree on how to report that. du counts it once within a single traversal and twice if you scan the two directories separately. Deleting one name frees nothing; the data goes only when the link count reaches zero. This is also why a large file can appear to survive deletion — see disk space.

You cannot hard-link a directory

The kernel forbids it, because a cycle in the directory tree would make it impossible to walk safely. Symlink directories instead. For something that behaves like a directory in two places at once, a bind mount is the real answer.

Quick reference

ln -s target link            # symlink (target first, like cp)
ln -sr target link           # symlink with a computed relative path
ln target link               # hard link
ln -sfn newtarget link       # repoint a directory symlink safely

readlink -f link             # resolve to the final path
ls -li                       # inode numbers and link counts
stat file                    # detail, including link count

find . -type l               # all symlinks
find . -xtype l              # broken symlinks
find / -samefile file        # every hard link to a file

Related

  • find — locating symlinks, and the broken ones in particular.
  • File permissions — why a symlink’s own permissions never matter.
  • Disk space — inodes, link counts, and files that will not free their space.
  • tar — how archives handle links, and when -h changes the answer.
  • mount --bind — when you genuinely need a directory in two places.