scripts/rollback.sh
deploy.sh already rolls back when a health check fails. This is for the other case: the deploy passed, and the problem surfaced ten minutes later.
APP_ROOT=/var/www/myapp ./rollback.sh --list
APP_ROOT=/var/www/myapp ./rollback.sh --dry-run
APP_ROOT=/var/www/myapp ./rollback.sh
APP_ROOT=/var/www/myapp ./rollback.sh --to 20260823T161500ZIt walks backwards from the live release rather than taking the second-newest, so rolling back twice in a row goes where you expect — taking [-2] would bounce you back to the broken release. Nothing is deleted, so rolling forward is just another rollback in the other direction.
If the rollback target is also unhealthy it reports and stops. Swapping again automatically would just flap between two broken releases.
examples/
nginx.conf— documents the two details that actually bite:disable_symlinks off, or nginx caches the resolved path and keeps serving the old release after a swap; and$realpath_rootforSCRIPT_FILENAME, or PHP-FPM's OPcache keys every release to one shared entry and a deploy serves a mix of old and new bytecode.deploy.service+myapp.env— systemd oneshot unit, for when the deploy user shouldn't have a login shell.
CI
Adds a rollback test job: dry-run inertness, sequential rollback, --to roll-forward, and rejection of an unknown release.