Atomic site updates
Whenever I have new photos to add to my website, it takes a while for rsync to upload them. It leaves a short time window, during which loading my website used to result in it being loaded only partially, with placeholders for photos which failed to load, because the HTML got updated before the new photos were uploaded. To solve this issue, I decided to dig into how to atomically update a static site. It turns out to be pretty easy.
The first step is to make the webroot a symlink. Neither Caddy, nor
Nginx cache the canonical path this symlink resolves to, which is very
fortunate. You place one version of your website in a directory next to
it and point the symlink at that directory. When you want to update the
website, you upload it into yet another directory next to it, create a
new symlink, which points to the new directory, and overwrite the
original symlink with the new one, using the rename()
syscall (which is atomic). You then delete the old directory.
To optimise the upload itself, you may use the rsync
--link-dest option to avoid transferring, or indeed even
writing new files, when they’re unchanged from the previous version.
Instead rsync will create hardlinks to the old files in the new
directory. It makes the transfer faster and avoids a temporary doubling
of disk use.
Now the code.
Caddyfileyakubin.com, www.yakubin.com { root * /website/current file_server }
$ ls -l /website
total 8
drwxr-xr-x 6 yakubin yakubin 512 Jan 15 11:22 I7wyFs
lrwxr-xr-x 1 yakubin yakubin 6 Jan 15 11:22 current -> I7wyFs
-rwxr-xr-x 1 yakubin yakubin 180 Jan 6 01:45 switch.sh/website/switch.sh#!/usr/bin/env sh set -euxo pipefail ln -s "$1" new-current mv -fh new-current current # If you're on Linux, replace "-fh" with "-fT" # Remove all directories except the one with the current website version find . -mindepth 1 -maxdepth 1 -type d | grep -Fxv "./$(readlink current)" | xargs rm -rf
FreeBSD’s mv has the -h option, which is
documented in its man page as follows:
-h If the target operand is a symbolic link to a directory, do not
follow it. This causes the mv utility to rename the file source
to the destination path target rather than moving source into the
directory referenced by target.On Linux the same thing is done with -T.
And now the upload script. It accepts a path to a local directory with the new website version as its first and only command line argument:
#!/usr/bin/env sh
set -eux
# Setting pipefail breaks this script.
directory="$(</dev/urandom LC_ALL=C tr -cd 'A-Za-z0-9' | head -c 6)"
rsync -crlDivz --link-dest=../current/ "$1"/ yakubin.com:"/website/$directory/"
ssh yakubin.com "cd /website; ./switch.sh $directory"