Linux Basic — Hard Links and Symlinks

4 minute read

Hard Links and Symlinks

Linux lets you create two kinds of links between filenames and files: hard links and symbolic links (symlinks). They look similar but behave very differently under the hood.


A hard link is a second directory entry pointing to the same inode. Both names are equal — neither is more “original” than the other.

# Create a hard link
ln source.txt hardlink.txt

# Verify: both files share the same inode number
ls -li source.txt hardlink.txt
# 2621441 -rw-r--r-- 2 ubuntu ubuntu 42 Apr  1 10:00 hardlink.txt
# 2621441 -rw-r--r-- 2 ubuntu ubuntu 42 Apr  1 10:00 source.txt
# ^^^^^^^ same inode!
# Link count = 2 (two directory entries point to this inode)

# Editing either file changes both (same underlying data)
echo "appended" >> source.txt
cat hardlink.txt
# appended

# Deleting one does NOT remove the data — inode persists until link count = 0
rm source.txt
cat hardlink.txt        # Still works!
ls -li hardlink.txt
# 2621441 -rw-r--r-- 1 ubuntu ubuntu 51 Apr  1 10:00 hardlink.txt
# Link count is now 1
# 1. Cannot hard link across different filesystems
ln /tmp/file.txt /home/ubuntu/file.txt   # Works (same filesystem)
ln /etc/hostname /mnt/usb/hostname       # ERROR: Invalid cross-device link

# 2. Cannot hard link a directory (prevents filesystem loops)
ln /home/ubuntu /home/ubuntu-link        # ERROR: hard link not allowed for directory

A symlink is a special file that stores a path string pointing to a target. It is like a shortcut or alias.

# Create a symlink
ln -s /path/to/target linkname

# Example: symlink to a config file
ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite

# Example: symlink to a directory
ln -s /opt/myapp-v2.1 /opt/myapp-current

# Inspect a symlink
ls -l /opt/myapp-current
# lrwxrwxrwx 1 root root 15 Apr  1 10:00 /opt/myapp-current -> /opt/myapp-v2.1
# ^
# └── l means symlink

# ls -la shows the target with an arrow
ls -li /opt/myapp-current /opt/myapp-v2.1 2>/dev/null
# The symlink has its OWN inode number (it is its own file)
# The symlink's content is the path string "/opt/myapp-v2.1"
# Following the symlink reads/writes the target's data

stat /opt/myapp-current
#   File: /opt/myapp-current -> /opt/myapp-v2.1
#   Size: 15               ← length of the path string
#   ...

Feature Hard Link Symbolic Link
Same inode ✅ Yes ❌ No (own inode)
Cross filesystem ❌ No ✅ Yes
Link directories ❌ No ✅ Yes
Survives target deletion ✅ Yes (data stays) ❌ No (becomes dangling)
Shows with ls -l Looks like normal file Shows -> arrow
Relative path support N/A ✅ Yes (relative or absolute)
Use case Backup, log rotation Deployment aliases, config enables

A symlink becomes dangling when its target no longer exists:

# Create a symlink and then remove the target
ln -s /tmp/missing.txt /tmp/mylink.txt
rm /tmp/missing.txt   # (or it never existed)

# Dangling symlink — ls shows it in RED in most terminals
ls -la /tmp/mylink.txt
# lrwxrwxrwx 1 ubuntu ubuntu 15 Apr  1 10:00 /tmp/mylink.txt -> /tmp/missing.txt

# Reading a dangling symlink fails
cat /tmp/mylink.txt
# cat: /tmp/mylink.txt: No such file or directory

# Find all dangling symlinks in a directory
find /tmp -xtype l
# /tmp/mylink.txt
# -xtype l = "type is symlink but the target does NOT exist or is a different type"

# Compare: -type l finds ALL symlinks (broken or not)
find /tmp -type l

# readlink — print the target of a symlink
readlink /usr/bin/python3
# python3.10

# readlink -f — resolve the FULL canonical path (follow all symlinks)
readlink -f /usr/bin/python3
# /usr/bin/python3.10

readlink -f /bin
# /usr/bin   (because /bin -> usr/bin on modern Ubuntu)

# realpath — same as readlink -f, more readable name
realpath /usr/bin/python3
# /usr/bin/python3.10

Use ln -sfn to atomically replace a symlink:

# -s: create symlink
# -f: force (overwrite if exists)
# -n: treat existing symlink as a file (not the target directory)

ln -sfn /opt/myapp-v2.1 /opt/myapp-current   # Atomically updates the alias

This pattern is the core of zero-downtime deployments — the application always reads /opt/myapp-current, and you just flip which version it points to.


The Versioned Deployment Pattern

# Deploy structure:
# /opt/
# ├── myapp-v1.0/   (old version)
# ├── myapp-v2.0/   (current version)
# ├── myapp-v2.1/   (new version, just uploaded)
# └── myapp-current -> myapp-v2.0  (active symlink)

# Deploy new version atomically
sudo ln -sfn /opt/myapp-v2.1 /opt/myapp-current

# Rollback is instant
sudo ln -sfn /opt/myapp-v2.0 /opt/myapp-current

# The running app reads /opt/myapp-current — it picks up the new version
# immediately without restart (for interpreted languages and config changes)

# Common in:
# - nginx configs (sites-available → sites-enabled)
# - Ruby on Rails Capistrano deployments
# - Python virtualenv management
# - Java JAR versioning

Practical Examples

# Enable an nginx site
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/mysite

# Disable it (remove the symlink only — config file stays)
sudo rm /etc/nginx/sites-enabled/mysite

# Create a convenient alias for a deep path
ln -s /var/log/nginx /home/ubuntu/logs

# Find all symlinks in /usr/bin
find /usr/bin -type l | head -10
# /usr/bin/python3 -> python3.10
# /usr/bin/awk -> gawk
# ...

# Find dangling symlinks anywhere in /opt
find /opt -xtype l 2>/dev/null

Key Takeaways

  • ln source link creates a hard link — same inode, cannot cross filesystems or link directories
  • ln -s target link creates a symlink — a file containing a path string, can cross filesystems and link directories
  • When a symlink’s target is removed, the symlink becomes dangling (broken) — find . -xtype l finds them
  • readlink -f and realpath resolve symlinks to their canonical absolute path
  • ln -sfn /new-target /symlink atomically updates a symlink — used in zero-downtime deployments
  • Nginx uses symlinks to enable/disable sites: sites-available/ → sites-enabled/

← Back to File Types and Inodes Next → Disk Usage: df and du

Updated: