Skip to content

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

  1. 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 name warning — behaviour does not change, both point at the same upstream.
  2. 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.