Creating Links (ln, symbolic vs hard)
Every file on a Linux filesystem is really two separate things: the data itself, and a name in a directory that points to that data. A link is just another name that points to existing data, either by pointing directly at the same underlying storage (a hard link) or by pointing at a path string that leads to another file (a symbolic link, or symlink). The ln command creates both kinds. Understanding the difference matters the moment you start organizing shared configuration files, installing software with multiple version directories, or writing scripts that reference files in more than one place.
Overview: How Links Work
To understand links, you first need to understand how Linux stores files. Every file’s actual content and metadata (size, permissions, owner, timestamps, and pointers to the data blocks on disk) live in a structure called an inode. Every inode on a filesystem has a unique inode number. Critically, the inode itself has no name — the filename you see in a directory listing is not part of the file’s data at all. It’s just an entry in a directory that maps a name to an inode number.
This separation is what makes links possible:
- A hard link is simply an additional directory entry pointing at the same inode number as an existing file. There is no “original” and “copy” — both names are equally real, equally valid, and point at exactly the same data. Each inode keeps a link count of how many directory entries point to it. The data on disk is only actually freed when that count drops to zero (i.e., every name pointing to it has been removed).
- A symbolic link is a completely different kind of object: its own small file, with its own inode, whose content is nothing more than a text string holding a path. When the kernel encounters a symlink while resolving a path, it reads that stored path and continues resolution from there — a process called dereferencing.
ls -lmarks these files with a leadinglin the permissions column and shows the target after an arrow, likelogfile -> /var/log/app.log.
Because hard links point directly at an inode, they only work within the same filesystem (inode numbers are only unique per-filesystem) and they cannot normally link to directories (this restriction exists to prevent loops in the directory tree that would break tools like find and rm -r). Symbolic links have neither limitation: they can cross filesystems, point at directories, point at something that doesn’t exist yet (a dangling symlink), or even point at a relative path that only resolves correctly from certain locations.
Syntax
ln [OPTIONS] TARGET LINK_NAME
ln [OPTIONS] TARGET... DIRECTORY
The first form creates one link named LINK_NAME pointing at TARGET. The second form links multiple targets into a directory, keeping their original basenames. The most important thing to remember is the argument order: TARGET (the file that already exists) comes first, LINK_NAME (the new name you’re creating) comes second — the same order as cp.
| Option | Meaning |
|---|---|
-s, --symbolic |
Create a symbolic link instead of a hard link (the default). |
-f, --force |
Remove an existing destination file before creating the link, instead of failing. |
-n |
When the link name is an existing symlink to a directory, treat it as a normal file rather than following it (avoids surprises with -f). |
-r, --relative |
With -s, automatically compute a relative path from the link’s location to the target, instead of using whatever path you typed. |
-v, --verbose |
Print each link as it’s created. |
-b |
Back up an existing destination file before overwriting it. |
Examples
Example 1: Creating a hard link
Suppose an application writes its log to /var/log/app.log, and you want a second name for that exact same data in the same directory, without duplicating the content on disk.
printf 'Server started\n' > /var/log/app.log
ln /var/log/app.log /var/log/app-backup.log
ls -li /var/log/app.log /var/log/app-backup.log
Output:
1234567 -rw-r--r-- 2 alice alice 15 Aug 4 10:15 /var/log/app-backup.log
1234567 -rw-r--r-- 2 alice alice 15 Aug 4 10:15 /var/log/app.log
The -i flag on ls prints the inode number as the first column: both filenames show inode 1234567, proving they are the same underlying file. Notice the link count (the number just before the owner name) is now 2 — two directory entries point at this inode. Writing to either name writes to the same data; there is no way to tell which one was created “first” just by looking at them.
Example 2: Creating a symbolic link
Now suppose you want a convenient shortcut in your home directory that points at that same log file, without creating another name on the same filesystem restriction that hard links require.
ln -s /var/log/app.log /home/alice/app.log
ls -l /home/alice/app.log
readlink /home/alice/app.log
Output:
lrwxrwxrwx 1 alice alice 15 Aug 4 10:20 /home/alice/app.log -> /var/log/app.log
/var/log/app.log
The permissions field starts with l, marking this as a symlink, and ls -l shows the arrow pointing at the stored target path. readlink prints exactly what’s stored inside the symlink — not the resolved absolute path of a chain of links (use readlink -f for that), just this one link’s immediate target.
Example 3: What happens when the target is removed
This is where hard links and symlinks behave very differently.
rm /var/log/app.log
cat /home/alice/app.log
cat /var/log/app-backup.log
Output:
cat: /home/alice/app.log: No such file or directory
Server started
Removing /var/log/app.log only removed that one directory entry. The symlink in Example 2 stored the path /var/log/app.log, and that path no longer resolves to anything, so it is now a dangling symlink — cat fails. But /var/log/app-backup.log, the hard link from Example 1, still works perfectly: it points directly at the inode holding Server started, and that inode’s link count simply dropped from 2 to 1. The data isn’t freed until the very last hard link to it is removed.
How It Works Step by Step
To see the inode relationship even more directly, hard-link an existing system file into /tmp (assuming both are on the same filesystem, which is typical on a single-partition Ubuntu install):
ln /etc/hosts /tmp/hosts-hardlink
ls -i /etc/hosts /tmp/hosts-hardlink
Output:
5678910 /etc/hosts
5678910 /tmp/hosts-hardlink
Walking through what the kernel actually did when you ran ln /etc/hosts /tmp/hosts-hardlink:
- The kernel resolves
/etc/hostsdown to its inode number (here,5678910) by walking the directory tree. - It creates a brand-new directory entry named
hosts-hardlinkinside/tmp, pointing at that same inode number — no new inode, no new data blocks, no copying. - It increments the link count stored in that inode’s metadata from 1 to 2.
Compare that to what happens for ln -s /var/log/app.log /home/alice/app.log in Example 2:
- The kernel allocates a brand-new inode for the symlink itself.
- It writes the literal text
/var/log/app.loginto that inode’s data (symlink targets are typically short enough to be stored directly in the inode, a trick called “fast symlinks”). - It creates a directory entry named
app.login/home/alicepointing at this new symlink inode — a completely separate inode from the log file’s own inode. - Any later access to
/home/alice/app.logmakes the kernel notice thelfile type, read the stored path, and re-resolve/var/log/app.logfrom scratch.
Common Mistakes
Mistake 1: Reversing target and link name
It’s easy to get the argument order backwards, especially coming from tools where you’d instinctively write “new name, old name.”
ln -s ~/bin/mytool /opt/mytool/bin/run.sh
This says “create a link named /opt/mytool/bin/run.sh pointing at ~/bin/mytool” — almost certainly backwards from what was intended, and if run.sh already exists as a real file, ln will refuse with File exists, which is actually a useful hint that the order is wrong. The corrected version, remembering that the existing target comes first:
ln -s /opt/mytool/bin/run.sh ~/bin/mytool
Mistake 2: Relative symlinks resolved from the wrong directory
A relative path inside a symlink is resolved starting from the symlink’s own directory, not from the directory you were in when you ran ln. This trips people up constantly:
cd /home/alice
ln -s ../shared/config.yml projects/myapp/config.yml
From /home/alice, ../shared/config.yml looks like it should mean /home/shared/config.yml. But the link that gets created lives at /home/alice/projects/myapp/config.yml, so the kernel will resolve ../shared/config.yml relative to that directory — landing on /home/alice/projects/shared/config.yml, which almost certainly doesn’t exist. The fix is either to use an absolute path, or let GNU ln compute the relative path correctly with -r:
ln -rs /home/alice/shared/config.yml projects/myapp/config.yml
Best Practices
- Use hard links only for plain files on the same filesystem when you specifically want two names that are truly indistinguishable copies of the same data (deduplication, backup snapshots).
- Use symbolic links for almost everything else: they work across filesystems, can point at directories, and are self-documenting since
ls -lshows exactly what they point to. - Prefer absolute targets for symlinks meant to be permanent, unambiguous references (e.g.
/etc/alternatives-style version switches). Prefer relative targets (orln -rs) for symlinks that live alongside their target inside a portable project directory, like a Git repository that might be cloned anywhere. - Check a suspicious file with
ls -lorfilebefore assuming it’s a regular file — scripts that blindlycator overwrite through a symlink can silently modify the linked-to file instead of the link itself. - Use
ln -sfto atomically replace an existing symlink with a new target, a common pattern for “current version” pointers in deployment scripts. - Remember
rmon a symlink removes only the link, never the target it points to — butrm -ron a directory whose contents include symlinks to directories will not follow those symlinks into the pointed-to directory by default.
Practice Exercises
- Exercise 1: Create a file
~/notes/todo.txtwith some text in it, then create a hard link to it named~/todo-link.txt. Confirm withls -lithat both share the same inode number and link count. Delete~/notes/todo.txtand verify the content is still readable through~/todo-link.txt. - Exercise 2: Create a symbolic link
~/configpointing at/etc/nginx(or any directory that exists on your system). Usels -lto confirm the arrow and target, then usecd ~/configandpwd -Pto see the fully resolved real path versuspwd -Lfor the symlink path. - Exercise 3: Create a dangling symlink on purpose by running
ln -s /tmp/does-not-exist-yet ~/broken-link. Runls -l ~/broken-linkand observe how your terminal or file manager flags it. Then create the target file and confirm the same symlink now resolves successfully without changing the link itself.
Summary
- Every file’s data lives in an inode; a filename is just a directory entry mapping a name to an inode number.
- A hard link is another directory entry pointing at the same inode — same file, same data, no “original” versus “copy” distinction.
- A symbolic link is its own inode whose content is a stored path string; the kernel re-resolves that path every time the link is accessed.
- Hard links only work within one filesystem and cannot target directories; symlinks can cross filesystems, target directories, and even point at nothing (dangling links).
ln TARGET LINK_NAMEcreates a hard link;ln -s TARGET LINK_NAMEcreates a symbolic link — the existing file always comes first.- Relative symlink targets resolve from the symlink’s own directory, not from wherever you ran
ln; use-rto let GNUlncompute this correctly.
