Deploy in one swap: no half-uploaded site, and rollback in one command
Copying over a live web folder serves half-uploaded pages. The folder swap this site uses, its small gap, and the symlink way that swaps instantly, with rollback.
- Never upload over what is being served. Prepare the new version completely elsewhere, then swap in one move
- A rename on the same disk is instant, with no in-between state, but a two-step swap still leaves a short gap
- Always keep the previous version; rollback should be one command you do not have to think about
The simplest deploy copies new files over the web folder. It mostly works, but during the few seconds files are arriving, a visitor may get the new HTML pointing at a CSS file that has not landed yet, so the page shows with no styling, or old HTML mixed with new JavaScript that fails in ways nobody can explain. This article shows how this site deploys every time, the small weakness that remains, and a method without it.
Tested on Ubuntu 24.04 with nginx. This site is a static Astro site of about 1.3MB.
The problem with copying over
Modern build tools name CSS and JavaScript files after their content, such as index.xB_X6hmL.css; change the content and the name changes. That lets browsers cache these files for a year, but it also means new HTML only works with the new files.
When copying over, files arrive in no particular order:
| Moment | In the web folder | A visitor right now |
|---|---|---|
| second 0 | all old files | fine |
| second 1 | new index.html, new CSS not here yet | unstyled page |
| second 2 | complete | fine |
Principle: never modify what is being served. Prepare the new version completely elsewhere, then swap in one move.
What this site does
The whole deploy is one script, run with npm run deploy:
npm run build
tar -C dist -czf /tmp/sixam-dist.tgz .
scp -q /tmp/sixam-dist.tgz prod:/tmp/sixam-dist.tgz
ssh prod 'set -e
rm -rf /tmp/sixam-new && mkdir -p /tmp/sixam-new
tar -C /tmp/sixam-new -xzf /tmp/sixam-dist.tgz
sudo rm -rf /var/www/6amdev.old
if [ -d /var/www/6amdev ]; then sudo mv /var/www/6amdev /var/www/6amdev.old; fi
sudo mv /tmp/sixam-new /var/www/6amdev
sudo chown -R www-data:www-data /var/www/6amdev
curl -s -o /dev/null -w "6amdev.com %{http_code}\n" https://6amdev.com/'
Step by step:
- Build locally and pack it into one file. One file transfers faster than hundreds of small ones, and cannot be half-sent and served
- Unpack into a new folder,
/tmp/sixam-new, which nobody sees yet. If this fails,set -estops the script and the live site is never touched - Move the old version to
.old, kept for rollback - Move the new version into place
- Check the site answers 200
Why mv is instant
mv within the same disk copies nothing; it only renames an entry in the filesystem’s directory. It happens at once however large the folder, with no in-between state: it has either been renamed or not.
But it must really be the same disk. If /tmp is another partition or lives in memory (tmpfs), mv becomes copy-then-delete, and the half-finished state is back. Check with:
df -h /tmp /var/www
On our machine both are on the same /dev/vda1.
The gap that remains
This is far better than copying over, but not perfect. Between step 3 and step 4 there is a fraction of a second when /var/www/6amdev does not exist at all, and a request landing exactly then gets a 404. The gap is tiny and acceptable for a knowledge site, but a system in constant use should close it completely.
One smaller point: chown runs after the swap. It belongs before it, so the new version is complete from the very first request.
The gapless way: a symlink
Instead of nginx pointing at a real folder, point it at a symlink called current, with each release in its own folder:
/var/www/6amdev/
├── releases/
│ ├── 20261015-1731/
│ └── 20261016-0930/
└── current -> releases/20261016-0930
nginx uses root /var/www/6amdev/current;. To deploy:
REL=/var/www/6amdev/releases/$(date +%Y%m%d-%H%M)
mkdir -p "$REL" && tar -C "$REL" -xzf /tmp/site.tgz
chown -R www-data:www-data "$REL"
ln -s "$REL" /var/www/6amdev/current.tmp
mv -T /var/www/6amdev/current.tmp /var/www/6amdev/current
The last line is the heart of it. mv -T renames the new symlink over the old one in a single rename, so every request sees either the old release or the new one, never nothing. Creating the link under a temporary name and then mv -T makes the swap a single rename for certain, without depending on how a particular tool version works inside.
Rolling back
Every method must roll back in one command, because when you need it you are in the middle of a problem and should not have to think.
With this site’s folder swap:
cd /var/www && sudo mv 6amdev 6amdev.bad && sudo mv 6amdev.old 6amdev
With symlinks, point current back at the previous release:
ln -s /var/www/6amdev/releases/20261015-1731 /var/www/6amdev/current.tmp
mv -T /var/www/6amdev/current.tmp /var/www/6amdev/current
The symlink layout keeps several releases; prune anything older than five now and then. The folder swap keeps just one, which is enough for a small site.
Check more than the home page afterwards
The script only checks that the home page returns 200, which is the bare minimum. Whenever pages are added we also check:
- The new pages, in both languages
- One CSS file, for its full set of headers (see the article on security headers)
- The other sites on the same machine still open, because several sites share this nginx
Common mistakes
- Copying or rsyncing over the live folder
- Unpacking into a tmpfs
/tmpand assumingmvis an instant rename - Keeping no previous version, so recovery means rebuilding while the site is down
- A rollback that takes several steps and some thinking
- Swapping a symlink by deleting the old one and then creating the new one, which leaves a moment with no symlink
- Checking only the home page after a deploy
FAQ
Does this work for sites with a backend?
The same principle applies, but the database needs thought too. If a deploy changes table structure, rolling back the code may not be enough. Design schema changes so old and new code both work during the transition.
Why not just use rsync?
rsync is great for transferring files, but rsyncing straight into the live folder still serves a half-finished state while files arrive. rsync into a new folder, then swap, and you get the best of both.
Commands and settings were tested on sample systems. Try them on a test machine before production.