nimory: My Internet at Home

nimory is a single machine running everything I need — files, photos, notes, scheduling, finance, and a self-hosted AI persona. No subscriptions. No third-party dependency for the things that matter.

This is a write-up of how it's built and how it works. It's had a full rewrite since the last version of this post — every custom app is now a small Go binary instead of Python, and two of them replaced three older ones.

nimory architecture


Hardware 🖥️

A Dell OptiPlex 7060 Micro. Small form factor, low power, silent enough to ignore.

Storage is partitioned with LVM:

nvme0n1
├── nvme0n1p1   512M   /boot/efi
├── nvme0n1p2   488M   /boot
└── nvme0n1p3   1.8T   LVM
    ├── nimory--vg-root    →  /
    └── nimory--vg-swap_1  →  swap

It runs continuously with minimal intervention and low power usage.


Access Model 🌐

Internet (Cloudflare Tunnel)

No ports are exposed on the router. nimory establishes an outbound encrypted tunnel.

Browser → Cloudflare DNS → Cloudflare Tunnel → cloudflared → Caddy → App

Admin (Tailscale)

SSH is private via WireGuard mesh:

tailscale ssh nimory

Local LAN

AdGuard resolves local domains:

Device → AdGuard DNS → Caddy → App

Access Summary


Reverse Proxy 🧭

All traffic passes through a single entry point: Caddy — the only container with published ports.

Security headers:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin

Public Domains

Nextcloud is gone — I never used it day-to-day, so a cloud.nihars.com file explorer wasn't replaced with anything. One less thing to maintain.

LAN Domains

Local mirrors of everything above at *.nimory, plus:


Containers 📦

Each service runs in isolation and communicates over a shared internal network.

Edge + Networking

Infrastructure

Files + Sync

Photos (Immich v3)

AI Runtime

Custom apps (all Go, all single static binaries)


The Five Custom Apps 🧩

Previous write-ups covered three home-built apps. There are five now, and all of them got rewritten from Python to Go this year — smaller images, faster rebuilds, no dependency trees to manage. Everything else on this machine — Caddy, Immich, Syncthing — is infrastructure in service of these:

Natlas is the web UI for the flat-file vault — events, subscriptions, inventory, health, birthdays. The thing I like most about the rewrite: plugins are now pure YAML files, not Python modules. A new kind of data is a config file, not code.

Kronos replaces the old Taskmaster — same nightly pipeline (archive rollover, inbox cleanup, subscriptions, event lifecycle, birthday sync, health site regeneration, daily briefing), same one-container-one-scheduler design, but it no longer talks to Telegram directly. More on that below.

Omnimo replaces the old Telegram bot and chat backend, combined into one binary. It's the AI persona — reachable as a Telegram bot I talk to directly — and it picked up a download manager (/download a link, video, or torrent straight into my share folder) along the way. Backed by a local Ollama model, with a paid API as an online fallback when I want it.

arthik is new: a from-scratch double-entry personal finance app, PWA, works offline. It replaces an older CSV-based version of itself and its old demo site, which is now just a read-only demo login instead of a separate container.

samanote is the notes webapp, also rewritten — markdown editor with live preview, directly over the same vault, no database or index underneath it.

All five still read and write plain YAML/Markdown/CSV files in the same Obsidian vault — no database, no API layer between most of them. That part hasn't changed and isn't going to: I can open any of these files by hand and understand exactly what's in it.

How three of them talk to each other without a database

This is the part I actually find interesting enough to write about. Natlas, Kronos, and Omnimo all touch the same vault, and two small conventions are what let them cooperate without stepping on each other:

arthik and samanote stay deliberately outside both of these — they don't need to notify me yet, so they don't carry the complexity of hooking into them.


Storage 💾

All persistent data lives on host:

/home/datar/
├── data/
│   ├── notes/       ← the vault: events, subscriptions, health, inventory, birthdays, outbox, finance
│   ├── documents/   ← Syncthing
│   ├── music/       ← Syncthing
│   ├── photos/      ← manual, selected subset only (not Syncthing)
│   ├── share/       ← Samba, LAN only
│   ├── backups/
│   └── workspace/
└── docker/

Automation ⚙️

Kronos runs its own scheduler inside one container, against a job list, and works through the same nightly pipeline as before:

archive rollover → inbox cleanup → subscriptions → event lifecycle
  → birthday sync (weekly) → event updates → regenerate health site
  → build daily briefing → hand it to Omnimo → Telegram

One real improvement worth a mention: date math around month-end now holds correctly for recurring things that fall on the 31st — it used to drift down to the 28th over a couple of months, it doesn't anymore.


Security Model 🔐

One deliberate exception, unchanged from before: the dashboard has read-only access to the host filesystem and the Docker socket, which is how it shows live container and system stats. It's the one container with that level of visibility, kept that way on purpose.

A new, narrow one: Immich's photo server now has passthrough access to the GPU device for hardware video transcoding. Worth a mental note, not a concern — QuickSync transcoding itself doesn't need broad privileges.

Attack surface is otherwise intentionally minimal.


Not Done Yet 🚧


nimory runs quietly and stays out of the way.

It's not complex for the sake of it. Just controlled, predictable, and entirely mine.

# Last updated on .