Work log¶
2026-07-27 — a record of the work that set up this documentation platform, changed the ownership model for the edge configuration, closed the operational weak points and translated all of the documentation into seven languages.
The purpose of this page is not to note what was done but why it was decided that way. Reasoning is the information that goes stale most slowly — the code may change, but the rationale behind a decision remains.
Scope
Operational details — host addresses, paths, secret configuration values — are not part of this public site; they live in a closed runbook inside the relevant repository.
Five strands¶
| # | Work | Outcome |
|---|---|---|
| 1 | Set up the documentation platform | docs.gerege.mn went live |
| 2 | Break up ownership of the edge configuration | Each domain moved into its own repository |
| 3 | Close operational weak points | Accidental deletion and blind spots removed |
| 4 | Translate the documentation into seven languages | Mongolian + the six official UN languages, full coverage |
| 5 | Record the move to Nexus | The ecosystem's new model documented in seven languages |
1. The documentation platform¶
What was done¶
An MkDocs Material portal that collects the ecosystem-level documentation of the Gerege ecosystem in one place: 25 pages, Mongolian as the default, Mermaid diagrams, brand CSS. The language coverage was later extended to seven languages — see section 4.
The content was gathered from the READMEs, docs/ directories and architecture
documents of the ecosystem repositories, and organised into four parts: layers ·
platforms · standards · operations.
Key decisions¶
Serve the static site from a separate container. Adding a new mount to the edge nginx container requires recreating it — and at that moment every domain briefly goes down. Serving from its own small container means the edge only needs a configuration addition and a reload.
Deploy in place, with rsync. The site directory is bind-mounted into the
container. Replacing the directory wholesale (mv) leaves the container still
looking at the old inode, so new content never appears at all. rsync updates
the files in place, so the mount stays valid.
Anchors for Cyrillic headings. Python-Markdown's standard toc slugify
deletes non-ASCII characters — the heading ## Танилт gets an empty id and
in-page links fail silently. We switched to the unicode-preserving
pymdownx.slugs.slugify.
validation.anchors switched on. In MkDocs, anchor checking is off by
default. Without it, a broken link pointing at #section passes the build and
reaches production. Now it fails under --strict.
Anything inside docs/ becomes public. Operational detail was therefore
placed separately, in a directory that is not part of the site.
2. Ownership of the edge configuration¶
This was the largest architectural change.
How it was before¶
Every domain's vhost lived in one central file. The consequence: changing a
small setting for docs.gerege.mn meant sending a PR to another repository and
waiting for another team's deploy. Ownership was blurred and changes were slow.
How it is now¶
the edge nginx configuration directory
├── (shared files) ← owned by the combined stack's repository
│ sso · dan · gsign · xyp
├── docs.gerege.mn.conf ← docs-gerege-mn
├── developer.gerege.mn.conf ← developer-gerege-mn
└── template.gerege.mn.conf ← template-gerege-mn
Each domain's vhost now lives in that service's repository, and its own deploy installs it. A change is finished inside a single repository.
Why it works¶
The configuration directory physically sits inside another repository's working
copy, and that repository's deploy runs git reset --hard. git reset --hard
only restores tracked files — it does not touch untracked ones. So a file
installed from an outside repository survives.
Three conditions for full independence¶
A vhost must depend on the shared files for nothing at all:
| Condition | Why |
|---|---|
Its own limit_req_zone |
Referring to a shared zone file creates a dependency on it |
Its own listen 80 block (ACME + redirect) |
Certificate renewal then works independently |
| Reinstalled on every deploy | If the file is lost, it restores itself |
A zero-downtime migration order¶
- First install the new file. At that point one domain is defined in two
places, but nginx handles this as nothing more than a
conflicting server namewarning — behaviour does not change, both point at the same upstream. - Then remove it from the central file. The duplication disappears and the new file takes effect.
Do it the other way round and the domain goes down between the two steps.
No host paths in an open-source repository¶
template-gerege-mn is open source, and the existing convention was to keep
server details only in CI secrets. So the installer discovers the path to the
configuration directory from the edge container's mounts itself:
docker inspect <edge> --format \
'{{range .Mounts}}{{if eq .Destination "/etc/nginx/conf.d"}}{{.Source}}{{end}}{{end}}'
This is more reliable than hard-coding the path, so it was later adopted in all three repositories — it keeps working when the host or the path changes.
A security precondition¶
Certificate FIRST, then the vhost. Adding an HTTPS vhost with no certificate
in place makes nginx -t fail, and at that moment every domain is at risk. The
ACME challenge runs through the general default server on port 80, so obtaining
a certificate requires no configuration change.
Before pushing, the final configuration was verified with nginx -t in a
temporary container, with the real network and certificates.
3. Operational hardening¶
Protection against accidental deletion¶
Files that hold production up were not tracked in any repository.
git reset --hard does not touch them, but git clean -fd deletes them.
| File | If lost | Fix |
|---|---|---|
| The three domains' vhosts | 3 domains go down at once | .gitignore |
| The host's compose override | Container detaches from the edge network → 502 | .gitignore + an example file |
Why .gitignore solves it: without -x, git clean skips ignored
files. That protects them without having to track them.
The compose override must not be tracked: compose reads it automatically, so
production settings would be forced onto every developer's local environment.
The live file therefore stays untracked on the host, and only a restoration
example is kept in the repository. That the example produces the same result as
the live configuration was confirmed by comparing docker compose config
output.
Health monitoring¶
There was a host monitoring script before, but two faults coincided: it was not registered in cron at all, and some of the containers it named had been renamed and no longer existed. The script silently skips a missing container, so nobody knew monitoring had stopped completely.
Key decisions in the new version:
Put the container's own healthcheck first. Its interval and retries are already tuned to that particular service.
Run the HTTP probe from inside the container. Checking everything through the edge would make every service look down whenever the edge went down, triggering a mass restart and masking the real fault. Now each check stands on its own.
A consecutive-failure threshold plus a cooldown. A transient stall (a deploy, GC, load) does not trigger a restart, and a genuinely broken service is not restarted over and over.
Never auto-restart stateful infrastructure. Databases and caches are only monitored and recorded. A restart does not fix a real cause such as a full disk; it cuts off transactions across many stacks and only adds damage. A human decides in that case.
Record a missing container as an error — so as not to repeat the main flaw of the previous version.
Threshold · cooldown · the stateful policy · recovery · a missing container — all five behaviours were verified for real on an isolated test container.
Certificate renewal¶
Seeing a cron entry is not enough — whether renewal actually runs was tested
with --dry-run, confirming that every domain renews successfully. This is the
kind of risk that stays silent until the expiry date.
4. Seven-language coverage¶
What was done¶
Every page of the site was translated into Mongolian plus the six official languages of the United Nations: العربية · 中文 · English · Français · Русский · Español. The English translation had previously been partial (home page, introduction, layers, the platform list, authentication); that was completed and five more languages added.
Key decisions¶
Mongolian remains the source. The other six are translations — the original is written in Mongolian and converted from there. With two "source" languages, content starts to diverge silently.
Translate page by page, not language by language. Converting one document into six languages at once keeps the terminology, the table rows and the structure identical. Going the other way — "English for every page first" — means the languages translated later will be chasing a source text that has already changed.
Technical notation was not translated. Domains, repository names, code, YAML, endpoint paths and standard names (OIDC · PKCE · RFC 3161) stay as they are in every language. Translating them would make them impossible to copy and run.
File names are not translated. It is platforms/sso.ru.md, not
платформы/sso.md. That way the URL path stays the same when switching
languages, and deep links arriving from other repositories keep working.
Arabic RTL was not done by hand. Material recognises the ar locale, sets
<html dir="rtl"> and flips the menu and content flow itself. Code blocks and
ASCII diagrams stay LTR — which is correct, because commands and URLs lose their
meaning if their direction is reversed.
The unicode slugify is now three times as important. The
pymdownx.slugs.slugify introduced for Cyrillic headings is now also holding
the anchors of Arabic, Chinese and Russian headings together. With the standard
slugify, every in-page link across six locales would have failed silently.
fallback_to_default stays on. Every page is translated now, so the fallback
never fires — but it remains the guarantee that the site stays whole when a new
page is added and its translation lags behind.
Scope¶
This policy applies to ecosystem-level documentation — that is, to this site only. The deep technical documentation of an individual platform (endpoint schemas, SDK reference) stays MN + EN inside its own repository; its readers are engineers already working in that repository, so widening the coverage would return little.
5. Recording the move to Nexus (2026-08-07)¶
What happened¶
The open-gerege-nexus repository was created on 2026-08-05, and on 08-07 the
platform was rebranded Gerege Nexus and moved to nexus.gerege.mn. Two forks
followed: sso-gerege-nexus (Gerege SSO) and eduge-mn-nexus (eduge.mn). Since
this changes the ecosystem's distribution model, the documentation was brought
up to date across all seven languages.
Key decisions¶
No existing platform page was deleted. Template, Gerege Platform, SSO and Kiosk are all still in production. A new page was added and the existing pages carry a notice stating which state is in force. Deleting a page would have erased the documentation of a running system.
Layer 3 was split into two generations. Nexus does not replace the Template — both sit on layer 3 at once. Adding a new layer would have drained the meaning from the layering rule ("never reach past a layer").
Nexus's own OIDC provider is NOT layer 2. Nexus carries an OAuth2/OIDC provider, but it serves that deployment's tenants and third-party clients. The ecosystem's route to identifying a citizen remains Gerege SSO. Without saying so, a reader would conclude that "Nexus has replaced SSO".
Every domain was checked by hand. nexus.gerege.mn, eduge.mn and
geregekiosk.mn were confirmed live through DNS, HTTP and their TLS certificates.
That surfaced two things: geregekiosk.mn now serves Nexus, and
open.gerege.mn has been dropped from that host's certificate, so HTTPS fails
on a name mismatch.
A stale promise was corrected. The Template page claimed autosync carries changes downstream daily; that automation was stopped fleet-wide on 2026-08-06. A false promise is worse than a missing fact: an engineer waits for a fix to travel on its own.
Two translation gaps were closed. The "Independent brand domains" section of the domain map turned out to be entirely absent from all six translations; all seven are now level.
Summary of decisions¶
| Decision | Rationale |
|---|---|
| Static site in its own container | Recreating the edge takes every domain down |
rsync, not mv |
A bind mount stays on the old inode |
| Unicode slugify | The standard slugify destroys anchors for Cyrillic headings |
Switch on validation.anchors |
Otherwise broken links reach production |
| The vhost in the service's repository | A change finishes inside one repository |
| A fully self-contained vhost | Depending on a shared file makes the split pointless |
| Install first, remove after | The other way round takes the domain down |
| Discover the path automatically | More reliable than hard-coding; leaves no path in an open-source repo |
Protect with .gitignore |
git clean skips ignored files |
| Do not track the override | Compose reads it automatically — production settings would apply locally |
| Probe from inside the container | Prevents a mass restart when the edge goes down |
| Never restart stateful services | A restart does not fix the cause, it adds damage |
| Mongolian as the single source language | With two "sources", content diverges silently |
| Translate by page, not by language | Terminology and structure stay identical across six languages |
| Do not translate code, domains or repo names | Translating them makes them impossible to copy and run |
| Do not translate file names | The URL path stays the same across languages, deep links keep working |
Each language on a sub-path (/ar/) |
Subdomains would grow the SAN · vhost · hreflang all at once |
Deliberately not done¶
The sso · dan · gsign · xyp domains were not split out. Their code
lives inside the combined stack's repository, so the central file already is
their own repository. There is no reason to split.
No separate mount was added to the edge container. That would have required editing the combined stack's compose file and, on top of that, recreating the container — briefly taking every domain down.
A Mongolian search index was not forced. lunr.js does not support
Mongolian, so search in the default locale works with standard tokenisation.
Writing a dedicated stemmer would cost more than it currently returns.
No separate domain or subdomain per language. Paths of the form /ar/ and
/zh/ are handled by one certificate, one vhost and one deploy. Moving to
subdomains would have grown the certificate SAN, the edge config and hreflang
all at once.
Remaining risk¶
The three domains' vhosts and the host override files are protected by
.gitignore, but if someone runs git clean -fdx (which does include
ignored files) they will be deleted. The fix is to run the relevant repository's
install script again — noted in each of the three repositories' runbooks.