Copying new files directly over a live static directory creates a period where visitors can receive HTML from one release and assets from another. A safer pattern stages one complete tree beside the live tree, verifies it, then changes directory names.

After same-filesystem and stage verification with one deployer, live contains old and stage contains new. Move one sends live to rollback, leaving live absent while stage and rollback remain. Move two sends stage to live. On success live contains new and rollback old; on failure live stays absent and an explicit restore is attempted. A failed restore requires inspection. Published results require public checks.
These two moves leave live absent between them; crashes can prolong the gap. Restore must be attempted and checked. Old asset URLs must remain reachable across separate browser requests.

Keep all release directories on one filesystem

Keep the live tree, staged tree, and rollback tree on the same filesystem. These fragments assume one Bash session, GNU Coreutils, and a single deployer with exclusive access to these names. Stop on a failed command or either side of the copy pipeline:

set -euo pipefail
df -P /srv/www

Create a uniquely named stage directory under that parent. Do not reuse an unresolved variable or broad wildcard as a deletion target.

stage=$(mktemp -d /srv/www/site.new.XXXXXX)

Populate and verify the stage

Copy the generated artifact into the empty directory, then check representative files. The final command displays a count; compare it with the verified artifact’s expected count or manifest rather than treating any number as a pass:

tar -C public -cf - . | tar -C "$stage" -xf -

test -s "$stage/index.html"
test -s "$stage/404.html"
find "$stage" -type f | wc -l

Run link and checksum checks against the staged path when the tooling supports a directory argument. Also verify that the serving user can traverse the directories and read the files: mktemp -d starts with restrictive permissions, and the extracted artifact must supply the intended modes. A stage that has not passed verification must never acquire the live name.

Switch names with an explicit rollback path

Resolve every path before the first move:

live=/srv/www/site
stamp=$(date -u +%Y%m%dT%H%M%SZ)
rollback=/srv/www/site.old-$stamp

test -d "$live" && test ! -L "$live" || exit 1
test -d "$stage" && test ! -L "$stage" || exit 1
test ! -e "$rollback" && test ! -L "$rollback" || exit 1
df -P "$live" "$stage"
live_device=$(stat -c %d -- "$live") || exit 1
stage_device=$(stat -c %d -- "$stage") || exit 1
test "$live_device" = "$stage_device" || exit 1

mv -T -- "$live" "$rollback" || exit 1
if mv -T -- "$stage" "$live"; then
  test -s "$live/index.html"
else
  mv -T -- "$rollback" "$live" || {
    printf 'Restore failed; inspect %s and %s before retrying.\n' "$rollback" "$live" >&2
    exit 1
  }
  exit 1
fi

This is a two-step switch, not one atomic replacement: the live path is absent between moves, and a crash can leave it absent until recovery. GNU mv -T prevents an existing directory from silently becoming a container for the moved tree; it does not serialize competing deployers. Do not proceed if the filesystem check differs. Platform-specific atomic exchange or symlink replacement can remove the missing-name interval, but must be tested with the actual server.

Verify the public edge

After the switch, request the homepage, one deep route, one fingerprinted asset, and a missing path. Compare public bytes with the staged artifact. Keep the rollback directory until that check and the observation window complete.

Even an atomic name switch does not make a browser’s HTML and asset requests one transaction. Keep older fingerprinted assets reachable for cached or in-flight HTML; merely retaining an unserved rollback directory does not do that. A filesystem rename also makes no promise of crash durability without the platform-specific persistence steps.

Sources