Skip to content
LotusMake

Back to The Aeon Forge

Post

Cosmic Mind: a hardened Quartz site for your Obsidian second brain

Cosmic Mind wraps Quartz v4 in Docker Compose, Caddy and a Cloudflare tunnel so one Obsidian vault becomes two sites: a private second brain and a public garden of the notes you mark. What is hardened, what broke on the way, and how to run it.

6 min read

by The Aeon Forge

A second brain is the plain name for a habit: every idea, reading note and half-finished argument goes into one place, in files you own, linked so old notes find you again. Mine is an Obsidian vault, a folder of Markdown files, and it was private by default. That was the problem. The notes worth sharing sat beside daily logs and inbox scraps, and hand-made copies went stale the moment I touched the originals.

Cosmic Mind is the deployment layer that turns the vault into a website, twice: an internal site with the full vault, and an external site with only the notes I have marked public. The README calls it a security and resiliency focused deployment of Quartz: a place to build your second mind and share it with the world. The repository holds the Compose services, scripts, Caddy web server configuration and static assets; the engine lives in a separate fork. The license is MIT.

github.com/AEON-7/cosmic-mind↗

AEON-7/cosmic-mind

A security and resiliency focused deployment of the Quartz web app. A place to build your second mind and share it with the world.

Shell · MIT · updated 2026-10-06

Load the latest from GitHub

What Quartz is

Quartz v4 is a static site generator: it reads a folder of Markdown and writes finished HTML, so nothing runs when a visitor arrives. It was written for digital gardens, collections of notes tended over time rather than published once. From an Obsidian vault it produces backlinks under each note, full-text search, and a graph view that draws every note as a node and every link as an edge.

My fork keeps its changes on a cosmic-mind branch, with a v4 branch tracking upstream. Most concern the graph, because the graph is what makes a garden legible to a stranger: hub notes scale with their connection count, labels stay visible, a hovered node spotlights its neighbors, and a legend and an "Expand Full Graph" button complete it. All of it is ordinary commits, which matters below.

One vault, two gardens

Nothing leaves the vault unless a note's frontmatter says publish: public or publish: external. A filter script clears a staging directory, copies only the flagged notes, and copies images and PDFs only from directories that hold a published note, so a private media folder never rides along. If no note is marked, the external build is skipped. Whole folders (private, .obsidian, daily-notes, inbox, templates, ai-interactions) are ignored, and analytics is null: no client-side tracking.

Publishing is opt-in, per note: the vault stays the private brain, and the flag decides which notes grow into the public garden. The design notes argue, and I agree, that this beats a second repository with notes moved across, which splits links, graph and history. The worst mistake left is forgetting to mark a note, and that fails safe.

What went wrong first

The first version patched the Quartz graph at build time with a shell script that edited upstream source. Such a patch breaks quietly whenever upstream changes the text it expects. The fork replaced it: the UI changes are commits, and an update is a rebase onto the new v4.x.x release and a rebuild of the builder image.

Watching a folder on Linux usually means inotify, the kernel's change notifications. The watcher polls instead, every 60 seconds by default (WATCH_INTERVAL), hashing the list of Markdown files and their modification times, which sidesteps inotify's watch limits and suits Syncthing, the peer-to-peer synchronizer writing into the same folder.

Three things I would change remain in the compose file. The watcher runs on alpine:latest and installs tools with apk at every start, and the builder is node:22-alpine; the lab now forbids Alpine images anywhere, so a rebuild moves both to digest-pinned Debian trixie and has the watcher reuse the builder image. The watcher also clones upstream Quartz, not the fork, when its engine volume is empty. And the Syncthing image is :latest, with peer ports 22000 (tcp and udp) and 21027 (udp) exposed directly; its web UI belongs behind single sign-on.

A build never breaks the live site

The builder refuses to run if the vault has no Markdown files, because an empty mount would publish a blank site. It renders its configs from .template files with envsubst, which substitutes environment variables, builds into a timestamped directory under builds/, and validates the result: index.html must exist and there must be at least 2 HTML files. Only then does it repoint the internal symbolic link by creating a temporary link and moving it over the live one with mv -Tf, an atomic rename: the link is either old or new, never missing. A failed build is deleted and the previous one keeps serving. The last 5 builds (BUILDS_TO_KEEP) are kept, pruning never touches the active one, and the vault is mounted read-only into the builder. The watcher also commits the vault to git before each build, so there is history to return to.

Security at the edge

From the internet the path is Cloudflare, then a Cloudflare tunnel, then a web application firewall in front of two Caddy replicas serving the static output. The WAF is Coraza with the OWASP Core Rule Set and an allowlist of known-valid paths regenerated from each build, so anything not in the site is refused at the edge. CrowdSec watches for hostile behavior, and GoAccess reads the logs for analytics without JavaScript. The WAF files are described in the README but were not in the repository when I looked; treat that layer as a design, not a drop-in.

Caddy serves on port 8080 with clean URLs, precompressed gzip and a set of headers: X-Frame-Options: DENY, nosniff, a Referrer-Policy, a Permissions-Policy that disables camera, microphone, geolocation, payment, USB and sensors, and no Server header. HSTS, which pins browsers to HTTPS, carries a max-age of 31536000 seconds, one year. The Content-Security-Policy, which says where scripts and styles may load from, allows the site plus Google Fonts and jsDelivr but must keep unsafe-inline and unsafe-eval for scripts because Quartz needs them, so the WAF and static-only serving carry that part of the defense. Cache lifetimes run from 5 minutes for HTML to 30 days for fonts.

Running it yourself

You need Docker with Compose v2, an Obsidian vault synced with Syncthing or mounted directly, and a Cloudflare tunnel or an exposed port. It needs no GPU and no model. Copy the environment file, set INTERNAL_DOMAIN, EXTERNAL_DOMAIN and the two site titles, then build and start:

Code, shellgit clone https://github.com/AEON-7/cosmic-mind.git
cd cosmic-mind
cp .env.example .env
chmod 600 .env
docker compose build quartz-builder
docker compose up -d syncthing vault-watcher
docker compose run --rm quartz-builder

The compose file carries host paths under /srv/appdata/secondbrain/, an external Docker network core_net and a books mount from my lab; replace them with yours. Script-header defaults differ from .env.example, so set your own domains. The rest of the library is in the catalog.

Sources

  • Matrix VoIP agent: call your AI, and let it see through your camera

    A headless Matrix VoIP agent that answers WebRTC calls from any Matrix client and talks back through local speech recognition, an LLM and streaming TTS, about 1.0 s to first speech on one DGX Spark, plus an optional camera look tool that keeps every frame in RAM.

    The Aeon Forge9 min read

Stoke the forge. From $4, straight to The Aeon Forge.