Pure static read-only Markdown viewer (midway-v2, nginx, amd64+arm64)
471
Staging branch build with dark mode, TOC, lightbox, KaTeX, search index, PWA.
Full docs in the MDViewer repo midway branch README.
Pure static, read-only Markdown viewer. No backend, no database.
Single nginx:alpine serves the frontend (mooloco/mdviewer:midway-v2,
multi-arch amd64/arm64); the docs directory is mounted read-only at
runtime (local bind or NFS). Rendering is done in the browser with
marked + DOMPurify + highlight.js, KaTeX loads on demand for math.
Deployed instances:
main branch image mooloco/mdviewer:v1): docs from NFS,
one shared web/ root.midway branch, this doc): verify frontend changes on a copy
of web/ with its own demo docs + own search-index.json before
syncing to production..md only, dotfiles hidden) with breadcrumb/search-index.json (title/path/snippet,
250 ms debounce, <mark> highlight), plus recent-files folder (max 10,
collapsed by default, independently scrollable)localStorage), font size standard/large, settings dialog at sidebar bottom$…$ / $$…$$ is detected
(code fences excluded, raw TeX kept on failure/offline)Markdown Viewer, offline favicon + PWA
(manifest.json, sw.js app-shell cache mdviewer-v2; /docs/* and
/search-index.json always hit network)GET/HEAD only, /docs/ read-onlyBrowser --GET /---------> nginx (/usr/share/nginx/html)
--GET /docs/----> autoindex JSON -> frontend file tree
--GET /docs/x.md> static md -> marked render
--GET /search-index.json -> static search index (per deployment!)
/docs <- runtime volume (local bind or nfs ro, pick one)
Only one docs source at a time, selected by DOCS_SOURCE.
scripts/compose.sh forces COMPOSE_FILE from DOCS_SOURCE so volumes
cannot be mixed by a stale .env.
Each deployment (production, staging) must have its own
web/root and its ownsearch-index.jsonbuilt from its own docs. Sharing one index across different docs roots makes search hits 404.
.
├── Dockerfile # builds nginx + web image (multi-arch ready)
├── docker-compose.yml # shared service (nginx + web + port)
├── docker-compose.local.yml # local directory source
├── docker-compose.nfs.yml # NFS volume source
├── .env.example # config template (committed)
├── .env # local runtime config (gitignored)
├── nginx/default.conf # read-only + autoindex json + .md MIME + index route
├── web/
│ ├── index.html
│ ├── app.js # routing/search/toc/lightbox/math/pwa/recent
│ ├── style.css # light/dark themes + responsive + touch
│ ├── favicon.png / apple-touch-icon.png / icon-192.png / icon-512.png
│ ├── manifest.json / sw.js # PWA (cache version mdviewer-v2)
│ └── vendor/ # 3rd-party static (gitignored, fetch it)
└── scripts/
├── compose.sh # docker compose wrapper by DOCS_SOURCE
├── fetch-vendor.sh # download vendor/*.js,*.css (incl. dark + katex)
├── build-index.py # build search-index.json from a docs dir
└── screenshot-demo.sh # optional screenshots (needs playwright)
Requires Docker (compose plugin) and curl. NFS mode additionally needs
nfs-common (Debian/Ubuntu) or nfs-utils (RHEL) on the host.
cp .env.example .env
# edit .env: pick DOCS_SOURCE=local|nfs, set paths below
sh scripts/fetch-vendor.sh
python3 scripts/build-index.py /path/to/docs web/search-index.json
sh scripts/compose.sh config
sh scripts/compose.sh up -d
curl -s http://127.0.0.1:8080/ | head
Open http://<host>:<port>/ in a browser.
The midway-v2 image below is multi-arch (linux/amd64, linux/arm64):
sh scripts/fetch-vendor.sh
python3 scripts/build-index.py /path/to/docs web/search-index.json
docker buildx build --platform linux/amd64,linux/arm64 \
-t mooloco/mdviewer:midway-v2 --push .
docker pull mooloco/mdviewer:midway-v2
docker run -d --name mdweb -p 8080:80 \
-v /path/to/your/docs:/usr/share/nginx/html/docs:ro \
mooloco/mdviewer:midway-v2
/docs inside the image is an empty placeholder. Mount any directory of
.md files read-only and it is served immediately. Rebuild
search-index.json from the mounted docs if you use sidebar search.
| Variable | Meaning | Default |
|---|---|---|
MDWEB_PORT | host-side listen port | 8080 |
DOCS_SOURCE | docs source, local or nfs only | nfs |
COMPOSE_FILE | forced by compose.sh from DOCS_SOURCE, manual edit ignored | — |
LOCAL_DOCS_PATH | local mode host absolute path | /path/to/docs |
NFS_SERVER | NFS server host/IP | nfs.example.com |
NFS_EXPORT | NFS export path (no colon; compose prepends :) | /export/docs |
NFS_VERSION | NFS version | 4.1 |
NFS_MOUNT_OPTIONS | mount options | ro,hard,timeo=600 |
Compose assembles the NFS options as (example values):
addr=<NFS_SERVER>,nfsvers=<NFS_VERSION>,proto=tcp,port=2049,<NFS_MOUNT_OPTIONS>,retrans=3
device: :<NFS_EXPORT> -> /usr/share/nginx/html/docs (ro)
Change port:
# .env
MDWEB_PORT=8081
sh scripts/compose.sh up -d
Sidebar search reads /search-index.json (static, built offline):
python3 scripts/build-index.py /path/to/docs web/search-index.json
# output: {"generated_at": "...", "files": [{"path": "/docs/a/b.md", "title": "...", "snippet": "..."}]}
Only .md files, dotfiles skipped, title = first H1 else filename,
snippet = Markdown-noise-stripped prefix (600 chars). Rebuild after docs
change (cron-friendly). Missing index degrades gracefully to directory
browsing with a notice.
Both containers serve the same frontend code but must not share web/:
/opt/mdweb/web -> mdweb:8080 -> NFS docs + NFS index
/opt/mdweb-staging/web -> mdweb-staging:8089 -> demo docs + demo index
Workflow: scp changed frontend files to staging first, verify on :8089,
then sync to production :8080. Rebuild each side's search-index.json
from its own docs root.
Server side (example), allow TCP 2049 for the viewer host:
# /etc/exports example:
# /export/docs 192.168.0.0/24(ro,sync,no_subtree_check)
exportfs -rav
Bare-metal mount test on the viewer host:
mount -t nfs -o vers=4.1,proto=tcp,port=2049,ro,hard,timeo=600,retrans=3 \
<NFS_SERVER>:<NFS_EXPORT> /mnt/nfs_test \
&& ls /mnt/nfs_test | head && umount /mnt/nfs_test
/usr/share/nginx/html/docs is mounted ro; touch inside reports
Read-only file system, which is expectednginx/default.conf denies everything except GET/HEAD.md is served as text/markdown, /docs/ and /search-index.json
are no-cache so source edits appear immediately| Symptom | Check |
|---|---|
mount: connection refused/timed out | server export + 2049/tcp firewall, exportfs -v |
wrong fs type / bad option | missing nfs-common/nfs-utils on viewer host |
| 403 / empty list | web/vendor fetched? docker exec mdweb ls -R /usr/share/nginx/html/docs |
| search hit 404s | index built from a different docs root; rebuild per deployment (see above) |
| port unreachable | ss -tlnp | grep <port>, change MDWEB_PORT then up -d |
| stale frontend / blank page | hard refresh (Ctrl+F5); SW cache versi |
Content type
Image
Digest
sha256:6e4ceb2a3…
Size
41.2 MB
Last updated
15 days ago
docker pull mooloco/mdviewer:midway-v2.2