Skip to content

Maintenance Edge-Proxy

The Maintenance Edge-Proxy (“edge”) is a ReadyStackGo-managed reverse-proxy container that runs per product in front of its public entry. It survives product redeploys and — driven by RSGO’s authoritative deploy state and the maintenance flag — either proxies transparently to the upstream or serves a controlled maintenance page plus a machine-readable status.

The feature is opt-in per product (via the manifest) and dormant by default: without an edge: block nothing changes about the existing behaviour.

During a redeploy or maintenance, RSGO removes the product containers — including the single public entry (e.g. the BFF/gateway). Clients and browsers then get “connection refused” instead of a meaningful page, and launchers/clients have no reliable way to tell why the service is down.

The edge closes that gap and cleanly separates two layers:

LayerResponsibilitySolved by
Control plane”Should the service be in maintenance?” — the toggleMaintenance Mode (flag/observer) — already present
Experience plane”What does the client see while the service is down?”Maintenance Edge-Proxy (this page)

Benefits:

  • No more connection-refused during redeploy or maintenance — the edge stays reachable throughout.
  • Controlled maintenance page instead of a browser error page, fully customizable.
  • Machine-readable status (GET /__status) so a launcher/client can robustly tell “running”, “maintenance” or “deploying”.
  • Generic: nothing product-specific — each product enables it via the manifest only.

RSGO runs the edge as a separate container (Caddy) managed in its lifecycle — it does not run inside the RSGO process (clean control/data-plane separation; an RSGO self-update causes no traffic blip).

Routing — from authoritative state, not guessed

Section titled “Routing — from authoritative state, not guessed”

The edge does not health-guess. Its only inputs are RSGO’s deploy state (ProductDeployment.Status) and the maintenance flag (OperationMode):

Deploy stateMaintenance flagEdge behaviour/__status state
RunningoffTransparent proxy to the upstreamrunning
RunningonMaintenance page, planned maintenancemaintenance
Deploying / Redeploying / UpgradinganyMaintenance page, temporarily unavailabledeploying
Failed / Stopped / otheroffMaintenance page, temporarily unavailablemaintenance

The flag only changes the wording (planned maintenance vs. temporarily down); whether to proxy is decided purely by the deploy state.

Survival — the edge outlives the redeploy

Section titled “Survival — the edge outlives the redeploy”

When tearing down a stack, RSGO removes all containers with the matching stack label. The edge (and an optional product-contributed maintenance container) carry survival labels and are excluded:

LabelValueMeaning
rsgo.scopeedgeContainer belongs to the edge scope → excluded from stack teardown
rsgo.redeployignoreGeneric opt-out → excluded from stack teardown
rsgo.rolemaintenance-pageProduct-contributed maintenance container (see below)

RSGO is also the single writer of the edge config: it computes the desired state and pushes it atomically and connection-preservingly via the Caddy admin API (POST /load) — without restarting the edge.


Step 1: Add the edge: block to the product manifest

Section titled “Step 1: Add the edge: block to the product manifest”

Add the optional product-level edge: block (sibling of metadata: / services:). The upstream service must be attached to the shared external network so the edge can reach it by DNS alias:

metadata:
name: ams.project
productVersion: "1.0.0"
networks:
ams-project-edge-net:
external: true # shared network edge <-> upstream
services:
web-bff: # public entry (BFF/gateway)
image: ams/bff:1.0.0
networks: [ams-project-edge-net]
edge:
enabled: true
publicHostname: project.customer.tld
publicPort: 443
upstream:
service: web-bff # = service/alias name on the network
port: 8080
network: ams-project-edge-net
tls:
mode: selfsigned
maintenancePage:
mode: default
branding:
productName: "ams.project"
supportContact: support@customer.tld
locales: [de, en]

Deploy as usual. RSGO detects the edge: block, provisions the edge container, and switches it to proxy once the product is Running. In normal operation the edge passes through transparently.

During a redeploy the edge stays up and serves the maintenance page (state: deploying) — no connection-refused:

Edge-proxy page during a rollout

With the maintenance flag set it shows the planned-maintenance variant including the reason:

Default maintenance page of the edge-proxy

The page renders the live /__status contract directly as a status panel, auto-refreshes every few seconds, and reloads into the app automatically as soon as the product reports running — visitors never have to reload by hand. The accent colour follows the state (amber for maintenance, blue for a rollout).

The machine-readable status at /__status distinguishes the states unambiguously:

/__status in maintenance

/__status in normal operation


There are three stages. The edge resolves them in the order container → bundle → default, falling back to the next stage.

maintenancePage.mode: default serves the built-in page — a live status panel in RSGO’s brand colours that mirrors the /__status contract (state, reason, version), polls for updates and switches users back to the app automatically once it is running. Customize it without any HTML via branding:

maintenancePage:
mode: default
branding:
productName: "ams.project"
logoUrl: https://customer.tld/logo.svg
supportContact: support@customer.tld
locales: [de, en]
FieldDescription
productNameDisplay name on the page / in the browser tab
logoUrlOptional logo (absolute URL)
supportContactOptional support address shown at the bottom
localesLanguages for the on-page toggle (e.g. [de, en])

maintenancePage.mode: bundle serves your own HTML page inline. RSGO reads it at deploy time from the manifest repository (bundlePath/index.html):

maintenancePage:
mode: bundle
bundlePath: ./maintenance/ # reads ./maintenance/index.html

b) Replacing it with your own container (full control)

Section titled “b) Replacing it with your own container (full control)”

For full customizability (own assets, dynamic content, your own web server) the product ships its own maintenance container that the edge proxies to during maintenance.

How:

  1. Set maintenancePage.mode: container and specify service + port.
  2. Label the maintenance service rsgo.role: maintenance-page and rsgo.redeploy: ignore (so it survives redeploys, like the edge).
  3. Attach the service to the shared edge network so the edge can reach it by alias.
networks:
ams-project-edge-net:
external: true
services:
maintenance-web:
image: ams/maintenance-page:1.0
networks: [ams-project-edge-net]
labels:
rsgo.role: maintenance-page
rsgo.redeploy: ignore # survives the redeploy
edge:
enabled: true
publicHostname: project.customer.tld
upstream: { service: web-bff, port: 8080 }
network: ams-project-edge-net
maintenancePage:
mode: container
container:
service: maintenance-web # = alias on the network
port: 80

During maintenance the edge forwards catch-all requests to maintenance-web:80. The GET /__status contract and the /hc passthrough stay unchanged — only the visible page comes from your container.


Ready-made templates (examples in the repo)

Section titled “Ready-made templates (examples in the repo)”

The repository ships a ready-to-use template for both customization paths under examples/edge-maintenance/:

VariantContentsFolder
BundleA self-contained, branded index.html + manifest snippetbundle/
ContainerDockerfile (nginx:alpine), assets (index.html, styles.css, logo.svg, status.js) + manifest snippetcontainer/

The example container (examples/edge-maintenance/container/) is a minimal nginx:alpine serving the html/ directory. Build and push it like this:

Terminal window
cd examples/edge-maintenance/container
docker build -t your-registry/ams-maintenance-page:1.0 .
docker push your-registry/ams-maintenance-page:1.0

Then reference the image in your manifest under maintenance-web (see the snippet above) and replace the assets in html/ with your own — the web server and port are up to you.

Live status: both templates poll /__status (same origin behind the edge) via a small script (status.js / inline). The page automatically switches its wording between “planned maintenance” and “deploying” and reloads once the product is running again.


FieldRequiredDefaultDescription
enabledyesfalseMaster switch. false/absent → feature inert.
publicHostnameyesHostname the edge serves (certificate CN; SNI key).
publicPortno443Public port the edge listens on.
imagenopinned caddyEdge image; pin a digest in production.
upstream.serviceyesInternal DNS alias of the public entry (e.g. the BFF).
upstream.portno8080Upstream port.
networkyesShared external network connecting edge and upstream (external: true).
mssnopmtuClient-facing TCP segment sizing for VPN robustness: pmtu (adaptive, default), a fixed number (e.g. 1360), or off. See VPN robustness below.
tls.modenononeTLS termination: selfsigned, custom (certRef), reuse (RSGO cert), letsencrypt.
tls.certReffor customReference to the uploaded certificate.
tls.letsencrypt.email / .dnsChallengefor letsencryptACME settings.
maintenancePage.modenodefaultdefault, bundle, or container.
maintenancePage.bundlePathfor bundleAsset directory in the manifest repo (index.html).
maintenancePage.container.service / .portfor container80Service alias + port of the maintenance container.
maintenancePage.branding.*noproductName, logoUrl, supportContact, locales.

When tls.mode is set, the edge terminates HTTPS on publicPort with a certificate that RSGO manages — the edge never runs ACME itself. Renewed certificates are reloaded without an edge restart.

tls.modeCertificate source
selfsignedRSGO generates a per-hostname self-signed cert and auto-renews it.
customOperator-provided certificate (certRef).
reuseRSGO’s own endpoint certificate (single-host case).
letsencryptUses RSGO’s ACME-managed certificate.

Full schema details: RSGo Manifest Format.


Products hosted behind the edge can be perfectly usable on the LAN yet fail over a VPN: the login (small requests) works, but loading a larger page or a big start-up configuration ends in an HTTP 502 or a timeout.

The cause is not the server — it is the path. A VPN tunnel wraps every packet in an extra header, which lowers the usable packet size (MTU). Normally the network signals “packet too large” back to the sender (Path MTU Discovery), but many corporate VPNs filter that ICMP message. Large response packets (sent with the don’t-fragment bit set) are then dropped silently — small transfers still fit, large ones stall. This is a classic PMTU blackhole.

Because the edge terminates the client TCP connection and is the public front door, network/MTU robustness belongs here — solved once at the edge, it applies to every RSGO-hosted product at every customer, with no change on the customer’s VPN.

edge:
enabled: true
# ...
mss: pmtu # default — you normally do not set this
ValueBehaviour
pmtuDefault, recommended. Adaptive kernel path-MTU probing (PLPMTUD, RFC 4821): the edge detects a blackhole on large responses and shrinks its TCP segment size on its own — without relying on the (filtered) ICMP message and without a guessed fixed value. Harmless on the LAN (full segment size when the path allows it).
a number, e.g. 1360Enforce a fixed maximum segment size. Use only if you must pin an exact value; valid range 536–1460. Applied by lowering the edge network’s MTU to mss + 40.
offDisable the feature entirely — the edge keeps the OS default (the behaviour before this option existed). Backward-compatibility escape hatch.

If mss is absent, invalid, or out of range, the edge falls back to pmtu — an unusable front door is worse than an ignored tuning value.

The default (pmtu) is implemented with namespaced kernel sysctls (net.ipv4.tcp_mtu_probing, net.ipv4.tcp_base_mss) set only on the edge container. The fixed mode lowers the edge network MTU. Neither mode requires the NET_ADMIN capability or any change to the container image — the edge runs with the same privileges as before.

Fixed-mode caveat: a fixed MSS is applied when RSGO creates the edge network. If the network already exists (e.g. it was created by the product stack first) its MTU cannot be changed without recreating it; RSGO logs a warning and the fixed cap does not take effect. In that case, prefer pmtu (which works regardless of who owns the network) or let RSGO create the network.


The edge serves a stable, versioned JSON at /__status — identical across all branding modes so a client/launcher can parse it robustly:

{
"schema": 1,
"state": "running",
"reason": null,
"until": null,
"productVersion": "1.0.0"
}
FieldMeaning
schemaContract version. Consumers should branch on it.
staterunning (proxying), maintenance (planned, reason set), or deploying (redeploy/upgrade in progress).
reasonPlanned-maintenance reason (from the flag), else null.
untilAnnounced end (ISO-8601) when available, else null.
productVersionCurrent product version when known.

This lets a client reliably distinguish “planned maintenance” (flag) from “redeploy/temporarily down” (deploy state) — without guessing.