Bash and Linux

Symlink the Current Release

mediumShell basics and files Must-do

Problem statement

Use a symbolic link called current to point at the live release, switch it to a new release, and roll it back, without copying any files. Many deploy setups work exactly like this: every release gets its own folder, the web server always reads current, and a deploy or rollback just moves the link.

The script creates three releases:

releases/

◈ DIAGRAM
releases/
├── 2026-09-01/version.txt (says: version 2026-09-01)
├── 2026-09-15/version.txt (says: version 2026-09-15)
└── 2026-10-01/version.txt (says: version 2026-10-01)

Do these steps in order:

  1. Find the newest release.
  2. Point current at it, then print where it points and the version it serves.
  3. Roll back: point current at the previous release, and print the same two things.
  4. Show what goes wrong if you switch the link without the -n flag, then clean up.
  5. Compare a hard link and a soft link: delete the original file and see which one still works.

Expected output:

◈ DIAGRAM
Newest release: 2026-10-01
current -> releases/2026-10-01
version 2026-10-01
current -> releases/2026-09-15
version 2026-09-15
Inside 2026-09-15 now: 2026-10-01 version.txt
current still -> releases/2026-09-15
hard.txt is the same file as original.txt
hard.txt still says: hello
soft.txt is broken: its target name is gone

Hints

Hint 1: ln -s target linkname makes a soft link. readlink linkname prints where it points.

Approach

Optimal: ln -sfn

Covers: ln, ln -s, ln -sfn, readlink, hard links, soft links, inodes, [[ a -ef b ]].

A link is a second way to reach a file. Linux has two kinds, and they work very differently.

Hard link: a second name for the same data. On disk, a file's data is stored once, and a name points at it. The data and its details (size, owner, permissions) live in a record called an inode. ln original.txt hard.txt adds a second name that points at the same inode. Neither name is the "real" one. The data stays until the last name is deleted.

Soft link (symlink): a note that says "go to this path". ln -s original.txt soft.txt makes a tiny file whose content is just the text original.txt. When you open soft.txt, Linux follows that path. If the path stops existing, the link is broken, like a shortcut to a deleted file.

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart LR subgraph HL["Hard link"] direction TB A["original.txt"]:::blue --> I[("the data")]:::yellow B2["hard.txt"]:::blue --> I end subgraph SL["Soft link"] direction TB S["soft.txt"]:::green --> P(["path:
original.txt"]):::purple P --> A2["original.txt"]:::blue A2 --> I2[("the data")]:::yellow end HL ~~~ SL classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style HL fill:transparent,stroke:#2563eb,stroke-width:2px style SL fill:transparent,stroke:#059669,stroke-width:2px
Hard link Soft link
What it stores points at the data itself stores a path
Original deleted still works broken
Can point at a folder no yes
Can cross to another disk no yes
How it looks in ls -l like a normal file soft.txt -> original.txt

Why deploys use soft links. A soft link can point at a folder, and switching it is a single quick step. The web server is set up once to serve /srv/app/current. A deploy copies the new release into its own folder, then moves current to it. A rollback moves current back. No files are copied at switch time, and the old releases stay on disk, ready to switch back.

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB W{{"web server
reads current/"}}:::gray --> C(["current"]):::purple C --> R3["2026-10-01
live"]:::green C -.-> R2["2026-09-15
rollback"]:::yellow R1["2026-09-01
old"]:::gray classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px

The three flags in ln -sfn.

Flag Meaning
-s make a soft link instead of a hard link
-f if current already exists, replace it
-n if current is a link to a folder, treat it as a plain name and do not go inside

Without -n, ln sees that current leads to a folder, so it thinks you mean "put a new link inside that folder". You end up with a stray link at releases/2026-09-15/2026-10-01, and current does not move at all. Step 4 of the code shows this on purpose.

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart LR subgraph BAD["ln -sf"] direction TB X["current already
points at a folder"]:::red --> Y["new link lands
inside that folder"]:::red --> Z["current
does not move"]:::red end subgraph GOOD["ln -sfn"] direction TB N["current already
points at a folder"]:::green --> M2["the link itself
is replaced"]:::green --> K["current points
at the new release"]:::green end BAD ~~~ GOOD classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style BAD fill:transparent,stroke:#dc2626,stroke-width:2px style GOOD fill:transparent,stroke:#059669,stroke-width:2px

Walking through the code. The # Setup: lines only create the release folders, so skip past them.

  1. ls -1 releases | sort | tail -n 1 lists the folder names, sorts them and keeps the last. Dates written as YYYY-MM-DD sort correctly as plain text, which is why release folders are often named this way. Reading ls output is fine here because the script made these names itself. For names you do not control, later pages use safer tools.
  2. ln -sfn "releases/$newest" current creates the link. readlink current prints the path stored in it, and cat current/version.txt reads through it.
  3. tail -n 2 | head -n 1 picks the second newest. ln -sfn moves current to it in one step.
  4. ln -sf without -n drops a stray link inside the folder. ls shows it, and readlink proves current did not move. rm removes the stray link.
  5. ln original.txt hard.txt makes a hard link and ln -s a soft one. [[ a -ef b ]] is true when two names are the same file on disk. After rm original.txt, the hard link still has the data, and [[ -e soft.txt ]] is false because its target is gone.

Edge cases. A link target is read relative to the folder the link sits in, not where you ran ln. Here current sits next to releases, so releases/2026-10-01 resolves correctly. If you moved current into another folder, the same relative target would break.

# Setup: three releases, each folder holds a version file
cd "$(mktemp -d)"
for r in 2026-09-01 2026-09-15 2026-10-01; do
  mkdir -p "releases/$r"
  echo "version $r" > "releases/$r/version.txt"
done

# 1. Find the newest release (dates in YYYY-MM-DD form sort correctly as text)
newest=$(ls -1 releases | sort | tail -n 1)
echo "Newest release: $newest"

# 2. Point "current" at it
ln -sfn "releases/$newest" current
echo "current -> $(readlink current)"
cat current/version.txt

# 3. Roll back to the previous release by moving the same link
previous=$(ls -1 releases | sort | tail -n 2 | head -n 1)
ln -sfn "releases/$previous" current
echo "current -> $(readlink current)"
cat current/version.txt

# 4. The trap: without -n, ln follows the old link into the folder
ln -sf "releases/$newest" current
echo "Inside $previous now: $(ls -1 "releases/$previous" | paste -sd ' ')"
rm "releases/$previous/$newest"         # clean up the stray link
echo "current still -> $(readlink current)"

# 5. Hard link vs soft link
echo "hello" > original.txt
ln original.txt hard.txt                # second name for the same data
ln -s original.txt soft.txt             # pointer to the name
[[ original.txt -ef hard.txt ]] && echo "hard.txt is the same file as original.txt"
rm original.txt
echo "hard.txt still says: $(cat hard.txt)"
[[ -e soft.txt ]] || echo "soft.txt is broken: its target name is gone"
RecapThe whole problem in a few lines, for the night before
  • Spot it: "switch versions instantly", "roll back a deploy", "point at the live release"
  • Idea: one folder per release and a soft link current; ln -sfn releases/X current moves it
  • Cost: switching is instant and copies nothing; old releases cost disk space until cleaned up
  • Trap: without -n, the new link lands inside the old target folder and current does not move

Interview follow-ups

  • How do you switch the link so no request ever sees a missing or half-made link?

    ln -sfn removes the old link and then creates the new one, so there is a tiny moment when current does not exist. On a busy server a request can land in that gap. The safe way is to build the new link under a temporary name and then rename it over the old one: ln -s releases/2026-10-01 current.tmp && mv -T current.tmp current. A rename on the same disk is atomic, which means it happens all at once. -T (GNU) tells mv to replace current itself, not move into the folder it points at.

  • How do you keep only the last few releases on disk?

    List the release folders oldest first, drop the newest ones you want to keep, and delete the rest. With date names, ls -1 releases | sort | head -n -3 (GNU) prints all but the newest three. Before deleting, make sure none of them is the one current points at, by comparing with readlink current. Run it with echo in front of rm -r first to see what would go.

Frequently asked questions

An absolute target like /srv/app/releases/2026-10-01 works wherever the link is, but breaks if the whole app folder is moved or mounted somewhere else, for example inside a container. A relative target like releases/2026-10-01 keeps working when the whole folder moves together, but breaks if the link alone is moved. For release folders that always live together, relative targets are common. Whatever you pick, readlink -f current prints the final full path, which is a good check after a deploy.

A hard link points straight at an inode, and inode numbers only mean something inside one filesystem. A disk mounted at /data has its own numbering, so a hard link cannot reach across. Folders are blocked on purpose, because hard-linked folders could create loops that tools like find and du could never finish walking. Soft links store a path instead, so they have neither limit. That is why almost all links you meet on servers are soft links.

ls -l shows links as name -> target, and the first character of the line is l. find . -type l lists every soft link under a folder, and find . -xtype l (GNU) lists only broken ones. readlink name prints the stored target, and readlink -f name follows every link to the end and prints the real path. Broken links are worth cleaning up, because tools that follow them will fail.