## Summary - Rename `date-modified` -> `modified` in all 80 docs and the `docs-check-frontmatter` task Quartz's `CreatedModifiedDate` plugin recognizes `modified`, `lastmod`, `updated`, and `last-modified` — but not `date-modified`. The wrong field name caused Quartz to ignore frontmatter dates entirely and fall through to filesystem timestamps (UTC inside Dagger), showing Feb 12 on pages built late on Feb 11 PST. ## Test plan - [x] `mise run docs-check-frontmatter` passes - [ ] Kick off docs release after merge — verify rendered dates match frontmatter values Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/158
6.5 KiB
| title | modified | last-reviewed | tags | ||
|---|---|---|---|---|---|
| Architecture | 2026-02-09 | 2026-02-09 |
|
Architecture Overview
Note: This article was drafted by AI and reviewed by Erich. I plan to rewrite all explanatory content in my own words - these serve as placeholders to establish the documentation structure.
How all the BlumeOps pieces fit together.
Physical Layer
Two always-on devices form the infrastructure backbone:
┌─────────────────┐ ┌─────────────────┐
│ Indri │ │ Sifaka │
│ Mac Mini M1 │────▶│ Synology NAS │
│ (compute) │ │ (storage) │
└─────────────────┘ └─────────────────┘
│
│ Tailscale
▼
┌─────────────────┐
│ Gilbert │
│ MacBook Air │
│ (workstation) │
└─────────────────┘
- indri runs all services (native and containerized)
- sifaka provides bulk storage and backup targets
- gilbert is the development workstation
Network Layer
tailscale provides the network fabric. All devices join a single tailnet (tail8d86e.ts.net) connected via WireGuard tunnels — no port forwarding or public IPs on homelab devices. ACLs control which devices and services can talk to each other, and MagicDNS provides *.tail8d86e.ts.net hostnames.
Routing Layer
Three layers of reverse proxying expose services at different scopes:
| Domain | Proxy | Reachable from |
|---|---|---|
*.tail8d86e.ts.net |
Tailscale MagicDNS | Tailnet clients only |
*.ops.eblu.me |
caddy on indri | k8s pods, containers, tailnet clients |
*.eblu.me |
flyio-proxy on Fly.io | Public internet |
Tailscale is the base layer — every service gets a MagicDNS hostname. The tailscale-operator gives Kubernetes services their own Tailscale Ingress endpoints.
caddy runs natively on indri and provides a unified *.ops.eblu.me wildcard with TLS (Let's Encrypt via DNS-01/Gandi). It proxies to both local services (Forgejo, Zot, Jellyfin) and Kubernetes services (via their Tailscale Ingress endpoints). Access is restricted by Tailscale ACLs — only tag:homelab and autogroup:admin can reach Caddy.
flyio-proxy runs on Fly.io for select services that need public internet access. Traffic hits Fly.io's Anycast edge, terminates TLS, and tunnels back to the homelab over Tailscale. Only services explicitly tagged tag:flyio-target are reachable — a compromised proxy cannot route to arbitrary services on the tailnet.
See routing for the full service URL table and port map.
Compute Layer
Services run in two places on indri:
Native (Ansible) — services that need host-level access run directly on macOS, managed via Ansible roles in ansible/roles/. See indri for the full list.
Kubernetes (ArgoCD) — most services run in minikube, managed via ArgoCD from argocd/manifests/. See apps for the application registry.
Data Flow
┌──────────────┐
│ Git Repo │
│ (Forgejo) │
└──────┬───────┘
│ push
▼
┌──────────────┐ ┌──────────────┐
│ ArgoCD │────▶│ Kubernetes │
│ (watches) │sync │ (runs) │
└──────────────┘ └──────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Service │ │ Service │ │ Service │
└──────────────┘ └──────────────┘ └──────────────┘
- Code pushed to forgejo
- argocd detects changes (or manual sync triggered)
- ArgoCD applies manifests to cluster
- Services start/update in Kubernetes
Observability
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Alloy │────▶│ Prometheus │────▶│ Grafana │
│ (collector) │ │ (metrics) │ │ (dashboards)│
└─────────────┘ └─────────────┘ └─────────────┘
│ ▲
│ ┌─────────────┐ │
└───────────▶│ Loki │────────────┘
│ (logs) │
└─────────────┘
alloy runs in three places:
- On indri: collects host metrics and logs
- In k8s: collects pod logs and service probes
- On flyio-proxy: tails nginx access logs and derives request metrics
See observability for details.
Secrets Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 1Password │────▶│ 1Password │────▶│ External │
│ (vault) │ │ Connect │ │ Secrets │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ K8s Secret │
└─────────────┘
Secrets live in 1Password and flow to Kubernetes via external-secrets.
For Ansible, secrets are fetched via op CLI in playbook pre_tasks.
Related
- why-gitops - Philosophy behind this approach
- security-model - Access control and secrets
- routing - Service routing details