Atomic Deployments — Flip a Switch, Not a Site
· Jerwin Arnado · 6 min read ·
Most deploy scripts have the same quiet flaw: they edit the live site in place. You copy new files over old ones while the server is still handing pages to visitors. For the few seconds that copy runs, the site is neither fully the old version nor fully the new one — it’s a mix, and a mix can be broken. An atomic deployment removes that window entirely. The switch from old to new happens in a single filesystem operation, so a visitor either gets the whole old site or the whole new site, never something in between.
The problem with copying in place
Picture the common shape: build the site, then sync the output straight into the directory the web server reads from.
# The naive deploy: overwrite the live directory in place
rsync -avz --delete ./build/ user@server:/var/www/site/
While that rsync runs, the live directory is a moving target:
| Failure | What a visitor sees |
|---|---|
| Half-copied assets | New HTML loads, but references a CSS/JS file not uploaded yet → unstyled or broken page |
The --delete window |
An old file is removed a split second before its replacement lands → a 404 on something that worked moments ago |
| Aborted transfer | Network drops mid-sync and the live site is left as a corrupt half-and-half |
| Bad build | The broken version is the live site now, with no clean previous copy to fall back to |
The root cause is the same in every row: the live site and the deploy target are the same directory. Atomic deployment breaks that coupling.
The core idea: a symlink is the switch
Instead of one live folder, keep many timestamped release folders and a single
symlink — call it current — that points at the active one. The web server’s document
root points at current, so visitors always follow the symlink to whichever release is
live.
/var/www/site/
├── releases/
│ ├── 20260716-140200/ ← previous release (kept for rollback)
│ ├── 20260716-151030/ ← previous release
│ └── 20260716-153312/ ← new release, just uploaded
└── current -> releases/20260716-153312 ← the only thing that "switches"
Deploying is now two independent halves:
- Upload the new build into a brand-new
releases/<timestamp>/folder. This can take as long as it needs — the live site is a different folder, completely untouched. - Switch by re-pointing
currentat the new folder.
That second step is the whole trick. Re-pointing a symlink is a single, atomic filesystem
operation. There is no moment where current points at “half the new release.” It points
at the old one, and then — one syscall later — the new one.
# The atomic switch: force + no-dereference re-points an existing symlink in one operation
ln -sfn releases/20260716-153312 current
The deploy flow, step by step
| # | Step | Touches live site? |
|---|---|---|
| 1 | Build locally | No |
| 2 | Upload build into a fresh releases/<timestamp>/ |
No — different folder |
| 3 | Smoke-check the new release (optional) | No |
| 4 | Re-point current → new release |
Yes — this instant only |
| 5 | Prune old releases, keep the last N | No |
Everything before step 4 is invisible to visitors. Step 4 is the deploy, and it’s instantaneous. Everything after is housekeeping. A minimal script:
#!/usr/bin/env bash
set -euo pipefail
BASE="/var/www/site"
RELEASE="$(date +%Y%m%d-%H%M%S)"
# 1–2. Build, then upload into a NEW release dir (note: no --delete needed).
build_site
rsync -avz ./build/ "user@server:${BASE}/releases/${RELEASE}/"
# 4–5. Switch atomically, then keep only the newest 5 releases.
ssh user@server bash -s <<EOF
set -euo pipefail
ln -sfn "${BASE}/releases/${RELEASE}" "${BASE}/current"
cd "${BASE}/releases" && ls -1dt */ | tail -n +6 | xargs -r rm -rf
EOF
Notice the --delete flag is gone. Each release is its own clean folder built from
scratch, so there are no stale files to purge — another class of bug removed for free.
Rollback becomes one command
This is the payoff that’s hard to overstate. Because previous releases are still sitting on disk, undoing a bad deploy is just pointing the symlink back:
ln -sfn releases/20260716-151030 current
No rebuild, no re-upload, no downtime — a second, at most. Compare that to the in-place approach, where “rollback” means rebuilding an old commit and re-running the whole risky sync. When something breaks at 2am, “flip the symlink back” is the difference between a shrug and an incident.
What it costs
Atomic deployment isn’t free, but for a static site the price is small:
| Trade-off | Detail |
|---|---|
| Disk | Each release is a full copy of the build. Static sites are tiny, so keeping 5–10 is cheap. Prune the rest. |
| Symlink support | The server must follow symlinks and the document root must point at current, not a fixed release path. |
| One-time setup | Create releases/ and the first current symlink by hand before the first deploy. |
| Not a cure-all | It won’t save you from a bad build (test before you switch) or from database migrations (those need their own careful, backward-compatible dance). |
The one-line version
Upload the new site next to the old one, then flip a symlink. The switch is instant, nothing breaks mid-deploy, and rollback is a single command.
This pattern is the backbone of tools like Capistrano and Deployer, but you don’t need any of them to get the benefit. A timestamped folder and a symlink is the entire idea — the rest is convenience.