Show a static maintenance page while the service restarts
A deployment's ErrorDocument-covered downtime window looked dead: Apache's default 502/503/504 error page, or a hung request, while instance-update briefly restarts the systemd unit. init --systemd now also writes a static werkator-maintenance.html next to the generated .htaccess, and the .htaccess maps a refused connection straight to it — served by Apache alone, no change needed to Werkator itself since it is exactly the process that is down. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
06967c551e
commit
ca9c23d55b
@@ -359,6 +359,13 @@ class InitCommand(
|
||||
normalizedWorkingDir.toFile(),
|
||||
)} (Apache reverse proxy; copy it into the domain docroot on a managed webspace)",
|
||||
)
|
||||
val maintenancePageFile = werkatorDir.resolve(SystemdServiceFiles.MAINTENANCE_PAGE_NAME)
|
||||
maintenancePageFile.toFile().writeText(SystemdServiceFiles.maintenancePageContent())
|
||||
println(
|
||||
"created ${maintenancePageFile.toFile().relativeTo(
|
||||
normalizedWorkingDir.toFile(),
|
||||
)} (shown by Apache while the service restarts; copy it next to the .htaccess)",
|
||||
)
|
||||
}
|
||||
|
||||
println("install and start the service and the nightly Docker cleanup with:")
|
||||
|
||||
@@ -13,6 +13,9 @@ object SystemdServiceFiles {
|
||||
/** Host-global unit names of the nightly Docker cleanup — shared by all Werkator repositories on the host. */
|
||||
const val HTACCESS_NAME = "werkator.htaccess"
|
||||
|
||||
/** Static page [maintenancePageContent] serves at, referenced by [htaccessContent]'s `ErrorDocument`s. */
|
||||
const val MAINTENANCE_PAGE_NAME = "werkator-maintenance.html"
|
||||
|
||||
const val PRUNE_SERVICE_NAME = "werkator-docker-prune.service"
|
||||
const val PRUNE_TIMER_NAME = "werkator-docker-prune.timer"
|
||||
|
||||
@@ -96,15 +99,53 @@ object SystemdServiceFiles {
|
||||
* Apache terminates TLS for the domain and forwards everything to the
|
||||
* localhost port of the "eigener Serverdienst". Generated host integration
|
||||
* like the units — the wrapper copies it into the domain's docroot.
|
||||
* `ErrorDocument` maps a refused connection (Apache's 502/503/504 while the
|
||||
* service restarts) to the static [maintenancePageContent] instead of Apache's
|
||||
* default error page; the `RewriteCond` keeps that one file from being proxied
|
||||
* itself, since `ErrorDocument` serves it as a sub-request through the same rules.
|
||||
*/
|
||||
fun htaccessContent(port: Int): String =
|
||||
"""
|
||||
DirectoryIndex disabled
|
||||
ErrorDocument 502 /$MAINTENANCE_PAGE_NAME
|
||||
ErrorDocument 503 /$MAINTENANCE_PAGE_NAME
|
||||
ErrorDocument 504 /$MAINTENANCE_PAGE_NAME
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{REQUEST_URI} !^/${MAINTENANCE_PAGE_NAME}${'$'}
|
||||
RewriteRule .* http://127.0.0.1:$port%{REQUEST_URI} [proxy]
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
/**
|
||||
* Static fallback for [htaccessContent]'s `ErrorDocument`s: shown by Apache directly,
|
||||
* without involving Werkator, during the brief window where the service restarts and
|
||||
* nothing listens on its port yet (`instance-update`). Self-contained — no external
|
||||
* assets, since nothing would be there to serve them while Werkator itself is down.
|
||||
*/
|
||||
fun maintenancePageContent(): String =
|
||||
"""
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Werkator — Maintenance</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #1e1e1e; color: #eee; display: flex;
|
||||
align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
div { text-align: center; }
|
||||
h1 { font-size: 1.4rem; margin-bottom: .5rem; }
|
||||
p { color: #aaa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1>Werkator is restarting</h1>
|
||||
<p>A deployment is in progress. Please retry in a few minutes.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun sanitize(name: String): String = name.replace(Regex("[^A-Za-z0-9_.-]"), "-")
|
||||
|
||||
/** Escape `%` specifiers in systemd unit values (legacy `systemd_path`). */
|
||||
|
||||
@@ -97,5 +97,26 @@ class SystemdServiceFilesTest : FunSpec() {
|
||||
content shouldContain "DirectoryIndex disabled"
|
||||
content shouldContain "RewriteRule .* http://127.0.0.1:18088%{REQUEST_URI} [proxy]"
|
||||
}
|
||||
|
||||
test("the htaccess maps a refused connection to the static maintenance page") {
|
||||
val content = SystemdServiceFiles.htaccessContent(18088)
|
||||
|
||||
content shouldContain "ErrorDocument 502 /werkator-maintenance.html"
|
||||
content shouldContain "ErrorDocument 503 /werkator-maintenance.html"
|
||||
content shouldContain "ErrorDocument 504 /werkator-maintenance.html"
|
||||
// the maintenance page itself must not be proxied, or ErrorDocument's sub-request loops
|
||||
content shouldContain "RewriteCond %{REQUEST_URI} !^/werkator-maintenance.html$"
|
||||
}
|
||||
|
||||
test("the maintenance page is a self-contained, static page naming a retry") {
|
||||
val content = SystemdServiceFiles.maintenancePageContent()
|
||||
|
||||
content shouldContain "<html"
|
||||
content shouldContain "restarting"
|
||||
content shouldContain "retry"
|
||||
content shouldNotContain "http://"
|
||||
content shouldNotContain "https://"
|
||||
content shouldNotContain "src="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user