Four WordPress sites run on one self-managed virtual private server (VPS), and a reload that Apache cannot parse takes all four down at once. The sites reached that box by a blue-green migration off managed hosting. On a box that carries four live sites, the distance between a routine reload and an outage is whether the configuration was validated first.
TL;DR
I gate every Apache reload, start, or rollback on sudo apache2ctl configtest returning Syntax OK, and I do not reload until it passes. A restored backup has to clear the same gate, or I put back a different known-good Apache backup or the OVH VPS snapshot restore instead. Before a deploy I take an Apache backup of the pre-change state and smoke-test the target domain immediately, so a regression surfaces at the deploy and not with a visitor.
The reload that trusts its configuration
The trap is that a reload is cheap to run and easy to trust. Apache configuration validation does not return Syntax OK, so the obvious answer, reload and see what happens, runs against invalid Apache or vhost syntax and takes sites down instead of refusing the change. The sites are live, and every web-server change passes through the same one-shot moment.
The constraint makes the failure worse: a failed reload can interrupt live vhosts, and a rollback archive may itself be invalid or incomplete. The recovery path can carry the same defect as the change it was meant to undo, and the window to catch a mistake is before the reload, not after it.
The decision: gate every change on apache2ctl configtest
The decision was to gate every Apache reload, start, or rollback with sudo apache2ctl configtest, and not to reload until it passes. One command, run before the change reaches production, converts a guess into a check. After a rollback the rule tightens: the restored backup must itself pass configtest before Apache reloads it, and if it does not, the fallback is a different known-good Apache backup or the OVH VPS snapshot restore, never a reload anyway.
The alternative was live and was rejected.
- Reload anyway after a failing configtest. Rejected because it propagates invalid config into a live server.
The fallback carries the same logic as the gate. A restored file is validated exactly as the change was, and a second known-good backup or the snapshot restore is the step when the restored state cannot clear the check.
The gate in practice
The lifecycle rule generalizes the decision. Nothing reloads or restarts Apache or PHP-FPM until the config gate passes: sudo apache2ctl configtest must return Syntax OK, and sudo php-fpm -t must return test is successful. For normal production changes I use sudo systemctl reload, not sudo systemctl restart, so active traffic is not dropped, and I reserve restart for a change that genuinely needs a full re-init. Every change ends the same way, with systemctl is-active confirming the service came back.
The gate runs the same way whoever makes the change: a config edit is followed by configtest, and only a passing test is followed by a reload. The order is the rule. The FPM-socket existence check, whether a site’s socket is present before its vhost is enabled, is a separate discipline that sits with per-site isolation.
The backup and the smoke test before a deploy
The gate checks syntax; a deploy can still regress, so a companion rule runs on both sides of it. Take an Apache backup of the pre-change state before any configuration deployment, then smoke-test the target domain immediately: curl -I https://DOMAIN/ should return HTTP/2 200. If it does not, the pre-change backup goes back in behind the same configtest gate, and Apache reloads from there.
The constraint is what forces the backup. A rollback requires a known-good pre-change Apache configuration, and a deployment regression can hit vhost or FPM state, so unless the pre-change state was captured first there is nothing recoverable to go back to. The deploy discipline is indifferent to what changed, whether a vhost, a directive, or a scheduled-work configuration.
What broke
Four incidents sit behind this discipline, and I state them as the records do. INC-001, INC-022, and INC-041 record configtest failing to return Syntax OK: a syntax error in an enabled vhost, or a rollback archive that was invalid or incomplete. INC-040 is a deployment followed by HTTP/2 500 and HTTP/2 502 responses from the affected domain, which is why the smoke test is immediate and not deferred to the next deploy. The lifecycle rule’s own record reports Broke: none, and I leave it stated that way.
What it costs to run
No figure is recorded for the time this costs, and I will not invent one. The discipline costs one config test per change and one smoke test per deploy, and the upkeep is the habit of running them. That is deliberate: a deterministic, human-owned rule, not autonomy. It holds only as long as the operator runs it.
What this does not solve
A syntax test is not a runtime test. A configuration that passes configtest can still regress at application, vhost, or FPM level, which is what INC-040 demonstrates, and a clean Syntax OK is no promise that the site will serve. That is why the pre-deploy backup and the immediate smoke test sit beside the gate rather than behind it, and why a rollback is only as good as the archive it restores: an archive that was invalid when it was taken will be invalid when it is put back. The discipline narrows the failure window; it does not close it.
Two adjacent subjects stay outside it, the scheduler that fires a personal automation and where a workload runs at all, local or cloud.
The operator behind this stack, and its trade-offs, are on the about page. If this is the shape of your problem, let’s talk.