Symlink the Current Release
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/
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:
- Find the newest release.
- Point
currentat it, then print where it points and the version it serves. - Roll back: point
currentat the previous release, and print the same two things. - Show what goes wrong if you switch the link without the
-nflag, then clean up. - Compare a hard link and a soft link: delete the original file and see which one still works.
Expected output:
Newest release: 2026-10-01current -> releases/2026-10-01version 2026-10-01current -> releases/2026-09-15version 2026-09-15Inside 2026-09-15 now: 2026-10-01 version.txtcurrent still -> releases/2026-09-15hard.txt is the same file as original.txthard.txt still says: hellosoft.txt is broken: its target name is goneHints
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.
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.
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.
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.
ls -1 releases | sort | tail -n 1lists the folder names, sorts them and keeps the last. Dates written asYYYY-MM-DDsort correctly as plain text, which is why release folders are often named this way. Readinglsoutput is fine here because the script made these names itself. For names you do not control, later pages use safer tools.ln -sfn "releases/$newest" currentcreates the link.readlink currentprints the path stored in it, andcat current/version.txtreads through it.tail -n 2 | head -n 1picks the second newest.ln -sfnmovescurrentto it in one step.ln -sfwithout-ndrops a stray link inside the folder.lsshows it, andreadlinkprovescurrentdid not move.rmremoves the stray link.ln original.txt hard.txtmakes a hard link andln -sa soft one.[[ a -ef b ]]is true when two names are the same file on disk. Afterrm 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 currentmoves 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 andcurrentdoes not move
Interview follow-ups
How do you switch the link so no request ever sees a missing or half-made link?
ln -sfnremoves the old link and then creates the new one, so there is a tiny moment whencurrentdoes 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) tellsmvto replacecurrentitself, 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 onecurrentpoints at, by comparing withreadlink current. Run it withechoin front ofrm -rfirst 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.