A maintenance page
A “down for maintenance” page is a good excuse to work at the lowest level:
just aptPackage, systemdService, file, and lineInFile — no container,
no service abstraction. The point is the shape: a parameterized Emet
function is the API surface you build for your fleet. Once you have
siteDownGlyphs, flipping a domain to maintenance is one call.
The template
htmlPath : String -> StringhtmlPath id = "/var/www/maintenance/${id}/index.html"
sitePath : String -> StringsitePath host = "/etc/nginx/sites-enabled/${host}.conf"
pageHtml : { host : String, reason : String, date : String } -> StringpageHtml cfg = String.join "\n" [ "<!DOCTYPE html>" , "<html lang=\"en\"><head><meta charset=\"UTF-8\">" , "<title>${cfg.host} — down for maintenance</title></head>" , "<body>" , " <h1>We'll be back soon</h1>" , " <p><strong>${cfg.host}</strong> is temporarily unavailable.</p>" , " <p>${cfg.reason}</p>" , " <p>Expected back: <strong>${cfg.date}</strong></p>" , "</body></html>" ]
siteConf : { host : String, id : String } -> StringsiteConf cfg = String.join "\n" [ "server {" , " listen 443 ssl http2;" , " server_name ${cfg.host};" , " ssl_certificate /etc/letsencrypt/live/${cfg.host}/fullchain.pem;" , " ssl_certificate_key /etc/letsencrypt/live/${cfg.host}/privkey.pem;" , " root /var/www/maintenance/${cfg.id};" , " location / { return 503; }" , " error_page 503 /index.html;" , "}" ]
siteDownGlyphs : { host : String, reason : String, date : String, id : String } -> List GlyphsiteDownGlyphs cfg = [ aptPackage { name = "nginx" } , systemdService { unit = "nginx.service" } , file { path = htmlPath cfg.id , contents = pageHtml { host = cfg.host, reason = cfg.reason, date = cfg.date } , mode = "0644" } , file { path = sitePath cfg.host , contents = siteConf { host = cfg.host, id = cfg.id } , mode = "0644" } ]Four glyphs, one function. Everything below the surface is String.join and
${…} — the language renders the HTML and the nginx config; the glyphs carry
the final bytes.
Two files, composed
The heart of the pattern is that an nginx site config is just a file. You
do not need a dedicated helper to write one — produce the content with string
interpolation and drop it where nginx expects it. The id flows through the
file paths, so two maintenance pages on the same host never collide:
/var/www/maintenance/litour-down-2026/index.html ^^^^^^^^^^^^^^^^^ cfg.idUsing it
main : List Scrollmain = [ scroll { name = "edge-01" , glyphs = siteDownGlyphs { host = "lichess.example.com" , reason = "Cleaning out the cobwebs" , date = "May 22nd, 2026" , id = "litour-down-2026" } } ]Save that as maintenance.emet, read the diff, then enact it:
emetc build maintenance.emet --text # the four glyphsgolemctl plan maintenance.emet ssh://golem@edge-01 # what would changegolemctl apply maintenance.emet ssh://golem@edge-01 # change itThe agent ensures nginx is installed and running, the HTML page is written, and the site config is in place.
Amending a file golem did not write, with lineInFile
The three file-and-package glyphs each own a whole resource. Sometimes you
only want to ensure one line is present in a file someone else owns —
that is lineInFile. Say the maintenance page’s server block must be
included from an nginx entrypoint the distro shipped:
includeLine : String -> LineInFileincludeLine host = lineInFile { path = "/etc/nginx/conf.d/maintenance.conf" , line = "include /etc/nginx/sites-enabled/${host}.conf;" }Add includeLine cfg.host to siteDownGlyphs and the agent appends that one
line if it is missing. On reverse it removes exactly that line, and only if
golem added it — a hand-added line, or another host’s include, is left
untouched.
That “someone else owns it” qualifier is the whole rule. Where golem owns both sides — several units contributing to one config — the answer is a drop-in directory with one complete file per contributor, not lines appended to a file they share (ADR 0041). See The four glyphs.
Taking it back
Delete the siteDownGlyphs { … } call from your scroll and re-apply. The
agent turns each glyph into a Remove:
- The maintenance HTML and the site config file are deleted (they were
Absentbefore golem wrote them). - The
includeline is removed — only because golem added it. nginxis removed only if golem installed it. If your real ingress was already using nginx, that other glyph still keeps it installed, and the reverse leaves it.
Removal restores what was there before, because the reversal receipt was captured at apply time. See Reversible reconcile.
Why the lowest glyphs matter
You could build this on top of a service or ingress abstraction and it
would be shorter. But those abstractions are also just file, aptPackage,
systemdService, and lineInFile underneath — the magic stops the moment you
write your own. Four glyphs are the floor; everything else is composition
above it.
See also
- The config — the Emet authoring surface.
- A service abstraction — the container-workload pattern.
- A tour of the lichess fleet — abstractions at fleet scale.