One double-click installer that turns a Windows PC into a private home library for someone who isn't technical.
Built for an 80-year-old dad: he clicks one icon, his books open. No accounts, no terminal, no cloud. Everything runs on his own computer in containers.
It bundles two open-source reading apps behind a single big-button home page:
| App | What it does | Self-hosted |
|---|---|---|
| Storyteller | Reads your ebooks with the audiobook narration in sync | ✅ official image |
| FlowState Reader | Calm, distraction-free focus reading | ✅ built from source |
Storyteller ships a clean, declarative docker-compose self-hosting recipe. FlowState is a lovely little app but only shipped as a GitHub Pages site with no container. Moonlight Oil takes Storyteller's compose approach and extends it to both apps in one stack, then hides the whole thing behind:
- a single installer that also sets up the container engine and WSL2, and
- a single large-print home page so there's one icon to click, not two setups.
That's the improvement: the non-technical person gets one turnkey thing instead of two half-technical ones.
- Download
MoonlightOilSetup.exeand double-click it. - Pick Docker (recommended) or Podman, click Next, wait.
- Restart the PC once (only the first time).
- Double-click Dad's Library on the desktop. Done.
The full, gentle walkthrough is in docs/for-dad.md.
- Turns on WSL2 (the Windows engine both Docker and Podman use).
- Installs Docker Desktop or Podman silently.
- Copies the stack to
C:\Users\Public\MoonlightOil, generates a secureSTORYTELLER_SECRET_KEY, and creates the desktop shortcut. - The shortcut runs
launch.ps1, which cold-starts the engine, runscompose up -d, waits until the page answers, and opens the browser.
Details for tinkerers: docs/self-hosting.md.
It's the only screen the person this is built for ever sees, so it does rather more than link to two ports:
- It knows what's actually running. Caddy proxies a same-origin probe for each service, so each button reads Starting up… or Ready rather than guessing. Clicking a service that isn't up yet explains itself instead of dropping you on a browser error page — which matters, because a cold container engine takes a minute and "This site can't be reached" is a dead end for someone who won't think to retry.
- It never hardcodes a port. Caddy publishes the real ones at
/config.json, so changing a port to dodge a conflict doesn't silently break the links. - It hands the library to a tablet. If the PC has an address on the home network, the page shows a QR code; point a phone camera at it and the library opens there, on the device someone actually wants to read on.
- The text gets bigger. Three sizes, remembered between visits, scaling the whole page rather than just the body copy. Plus light/dark.
The QR code is generated by stack/home/qr.js — a
from-scratch byte-mode encoder (Reed-Solomon, mask selection, the lot) in about
400 readable lines. A CDN would break the premise that nothing leaves the house,
and a minified blob would leave nobody able to read it.
installer/
moonlight-oil.iss Inno Setup script → compiles to MoonlightOilSetup.exe
AFTER_INSTALL.txt the friendly "you're done, restart once" note
assets/library.ico desktop icon
scripts/ PowerShell: install, launch, stop, uninstall, common
stack/
docker-compose.yml home + Storyteller + FlowState
.env.example documented settings (installer generates the real .env)
caddy/Caddyfile serves the home page; adds /config.json and /up/<service>
home/index.html the big-button landing page
home/boot.js applies the saved theme before the first paint
home/app.js live status, links, text size, tablet hand-off
home/qr.js dependency-free QR encoder (see below)
flowstate/Dockerfile builds FlowState Reader from source
site/ VitePress documentation site
tests/ validation suite (node:test) + a browser suite
docs/ standalone markdown guides
.github/workflows/ CI pipelines (build + test)
You don't have to — every push builds MoonlightOilSetup.exe in GitHub Actions
(download it from the run's Artifacts, or from a v* tag's Release). To
build locally on Windows with Inno Setup 6:
iscc installer\moonlight-oil.iss
The .exe lands in dist\.
npm test186 tests, no Docker and no dependencies required. They cover the compose config, the Caddy config, the home page's markup and behaviour, the Dockerfile, the installer scripts, and the environment file — plus 58 for the QR encoder, which check its output against the constants printed in ISO/IEC 18004, verify that every Reed-Solomon block has zero syndromes, and read the payload back out of the finished matrix the way a decoder would.
To also drive the real page in a real browser:
npm run caddy:check # ask Caddy whether the Caddyfile is valid
npm run home:up # start just the home service
npm run test:browser # 22 checks in headless Chromium
npm run home:downThat suite deliberately runs with Storyteller and FlowState down, because
"not listening yet" is the state the home page exists to handle well. It has
already earned its keep: it caught a Content-Security-Policy that blocked the
page's own startup script, and an aria-disabled attribute that made a
still-clickable card unreachable to assistive technology.
cd site
npm install
npm run devBuilds with VitePress. Covers architecture, installation, self-hosting, branching strategy, CI/CD, and testing.
See CONTRIBUTING.md for the branching strategy, CI checks,
and how to set up locally.
This project only orchestrates other people's work — it doesn't copy their code. Storyteller and FlowState Reader are each under their own licenses; please respect them. FlowState is fetched and built from its upstream repository at image-build time.