2026-01-25 09:35:06 -08:00
|
|
|
---
|
|
|
|
|
# Caddy reverse proxy configuration
|
2026-03-15 10:33:48 -07:00
|
|
|
# Caddy is built from ~/code/3rd/caddy with Gandi DNS and Layer 4 plugins
|
2026-01-25 09:35:06 -08:00
|
|
|
|
|
|
|
|
caddy_repo_dir: /Users/erichblume/code/3rd/caddy
|
|
|
|
|
caddy_binary: "{{ caddy_repo_dir }}/bin/caddy"
|
|
|
|
|
caddy_config_dir: /Users/erichblume/.config/caddy
|
|
|
|
|
caddy_data_dir: /Users/erichblume/.local/share/caddy
|
|
|
|
|
caddy_log_dir: /Users/erichblume/Library/Logs
|
|
|
|
|
|
|
|
|
|
# Gandi API token file (written by ansible, chmod 0600)
|
|
|
|
|
# Caddy reads this file for ACME DNS-01 challenges
|
|
|
|
|
caddy_gandi_token_file: /Users/erichblume/.config/caddy/gandi-token
|
|
|
|
|
|
|
|
|
|
# Domain configuration
|
|
|
|
|
caddy_domain: ops.eblu.me
|
|
|
|
|
|
2026-01-25 11:37:23 -08:00
|
|
|
# HTTPS port (443 is standard)
|
|
|
|
|
caddy_https_port: 443
|
2026-01-25 09:35:06 -08:00
|
|
|
|
|
|
|
|
# Services to proxy
|
|
|
|
|
# Format: { name: "service", host: "hostname", backend: "url" }
|
|
|
|
|
caddy_services:
|
|
|
|
|
# Indri-local services
|
|
|
|
|
- name: forge
|
|
|
|
|
host: "forge.{{ caddy_domain }}"
|
|
|
|
|
backend: "http://localhost:3001"
|
|
|
|
|
- name: registry
|
|
|
|
|
host: "registry.{{ caddy_domain }}"
|
|
|
|
|
backend: "http://localhost:5050"
|
2026-01-30 16:57:26 -08:00
|
|
|
- name: jellyfin
|
|
|
|
|
host: "jellyfin.{{ caddy_domain }}"
|
|
|
|
|
backend: "http://localhost:8096"
|
2026-01-25 09:35:06 -08:00
|
|
|
|
2026-01-25 12:56:31 -08:00
|
|
|
# K8s services (via Tailscale Ingress)
|
|
|
|
|
# Caddy proxies to existing Tailscale endpoints - traffic stays local
|
|
|
|
|
- name: grafana
|
|
|
|
|
host: "grafana.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://grafana.tail8d86e.ts.net"
|
|
|
|
|
- name: argocd
|
|
|
|
|
host: "argocd.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://argocd.tail8d86e.ts.net"
|
|
|
|
|
- name: prometheus
|
|
|
|
|
host: "prometheus.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://prometheus.tail8d86e.ts.net"
|
|
|
|
|
- name: loki
|
|
|
|
|
host: "loki.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://loki.tail8d86e.ts.net"
|
|
|
|
|
- name: miniflux
|
|
|
|
|
host: "feed.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://feed.tail8d86e.ts.net"
|
|
|
|
|
- name: devpi
|
|
|
|
|
host: "pypi.{{ caddy_domain }}"
|
2026-04-29 13:38:36 -07:00
|
|
|
backend: "http://localhost:3141"
|
2026-01-25 12:56:31 -08:00
|
|
|
- name: kiwix
|
|
|
|
|
host: "kiwix.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://kiwix.tail8d86e.ts.net"
|
|
|
|
|
- name: torrent
|
|
|
|
|
host: "torrent.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://torrent.tail8d86e.ts.net"
|
|
|
|
|
- name: teslamate
|
|
|
|
|
host: "tesla.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://tesla.tail8d86e.ts.net"
|
2026-01-26 11:20:11 -08:00
|
|
|
- name: immich
|
|
|
|
|
host: "photos.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://photos.tail8d86e.ts.net"
|
2026-01-31 20:19:31 -08:00
|
|
|
- name: navidrome
|
|
|
|
|
host: "dj.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://dj.tail8d86e.ts.net"
|
Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify (#190)
## Summary
Deploy a cloud-free NVR stack for the GableCam (ReoLink Elite Floodlight at 192.168.1.159):
- **Mosquitto** — shared MQTT broker in `mqtt` namespace (cluster-internal, no auth)
- **Ntfy** — self-hosted push notifications in `ntfy` namespace, exposed at `ntfy.tail8d86e.ts.net` / `ntfy.ops.eblu.me`
- **Frigate** — NVR with GableCam via HTTP-FLV, ONNX CPU detection, NFS recordings on sifaka, exposed at `nvr.tail8d86e.ts.net` / `nvr.ops.eblu.me`
- **frigate-notify** — bridges Frigate detection events (person, car, dog, cat) to Ntfy alerts via MQTT
Also includes:
- Prometheus scrape target for Frigate metrics
- Grafana dashboard for Frigate (status, inference speed, FPS, CPU/memory, storage)
- Caddy reverse proxy entries for `nvr.ops.eblu.me` and `ntfy.ops.eblu.me`
## Prerequisites
- [ ] Create NFS share `frigate` on sifaka (`/volume1/frigate`, RW for indri)
- [ ] Create 1Password item "Reolink Floodlight Camera" in `blumeops` vault with `username` and `password` fields
## Deployment (after merge)
```bash
argocd app sync apps
argocd app sync mosquitto
argocd app sync ntfy
argocd app sync frigate
argocd app sync grafana-config
argocd app sync prometheus
mise run provision-indri -- --tags caddy
mise run services-check
```
## Verification
- [ ] Mosquitto pod running, accepting connections on 1883
- [ ] Ntfy web UI accessible at `ntfy.ops.eblu.me`
- [ ] Frigate web UI at `nvr.ops.eblu.me` showing GableCam live feed
- [ ] Object detection working (ONNX, person/car/dog/cat)
- [ ] Recordings appearing in NFS share on sifaka
- [ ] frigate-notify sending detection alerts to Ntfy
- [ ] Prometheus scraping Frigate metrics
- [ ] Grafana dashboard showing Frigate data
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/190
2026-02-14 21:27:44 -08:00
|
|
|
- name: homepage
|
Add Hajimari service dashboard (#73)
## Summary
- Add Hajimari as a service dashboard/start page at `go.ops.eblu.me`
- Auto-discovers k8s services from ingress annotations
- Custom apps for non-k8s services: Forgejo, Registry, Sifaka NAS
- Add `nas.ops.eblu.me` Caddy proxy to Synology dashboard
## Services Configured
**Auto-discovered (k8s ingresses with hajimari.io annotations):**
- Grafana, ArgoCD, Prometheus, Loki (Observability)
- Miniflux, Kiwix, Transmission, TeslaMate, Immich (Apps)
- PyPI/devpi (Infrastructure)
**Custom apps (non-k8s):**
- Forgejo (forge.ops.eblu.me)
- Registry (registry.ops.eblu.me)
- Sifaka NAS (nas.ops.eblu.me)
**Bookmarks:**
- Tailscale Admin, 1Password, Pulumi
## Deployment and Testing
- [ ] Sync `apps` application to pick up new Hajimari Application
- [ ] Sync `hajimari` application
- [ ] Run `mise run provision-indri -- --tags caddy` for go/nas proxy entries
- [ ] Re-sync all k8s apps with hajimari annotations (or wait for natural drift)
- [ ] Verify https://go.ops.eblu.me shows dashboard with all services
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/73
2026-01-29 15:51:42 -08:00
|
|
|
host: "go.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://go.tail8d86e.ts.net"
|
2026-02-03 10:52:20 -08:00
|
|
|
- name: docs
|
|
|
|
|
host: "docs.{{ caddy_domain }}"
|
C1: migrate cv + docs from minikube to indri-native (#342)
## Summary
Replace the cv (`cv.eblu.me`) and docs (`docs.eblu.me`) minikube Deployments with indri-native ansible roles. Caddy serves the extracted release tarballs directly via a new `kind: static` service-block — no daemon, no nginx pod, no ProxyGroup ingress on the request path. Mirrors the rationale of the recent devpi migration; part of the broader minikube wind-down.
## What's in this commit
- `ansible/roles/{cv,docs}` — sentinel-gated tarball download + extract into `~/{cv,docs}/content/`
- `ansible/roles/caddy/` — new `kind: static` branch in the Caddyfile template (encoded gzip, immutable cache headers for fingerprinted assets, optional `try_html` for Quartz-style clean URLs, optional per-path `download_paths` for the resume PDF's `Content-Disposition`)
- `ansible/playbooks/indri.yml` — wires `cv` and `docs` roles before `caddy`
- `service-versions.yaml` — both services flip to `type: ansible`. `docs.current-version` stays at `1.28.2` for this commit so `container-version-check` keeps passing while `containers/quartz/Dockerfile` still exists; it moves to the docs release tag in the cleanup commit
- `.forgejo/workflows/{cv-deploy,build-blumeops}.yaml` — deploy step now bumps `cv_version`/`docs_version` in the role defaults and pushes; running ansible + purging the Fly cache is manual from gilbert (matches devpi)
- Docs: `docs/how-to/operations/{cv,docs}-on-indri.md`, updated `docs/reference/services/{cv,docs}.md`, changelog fragment
## What is not in this commit
The dead artifacts. After PR review and successful cutover, a follow-up commit deletes:
- `argocd/apps/{cv,docs}.yaml` and `argocd/manifests/{cv,docs}/`
- `containers/cv/`, `containers/quartz/`
- `CONTAINER_TO_SERVICE['quartz']` mapping in `mise-tasks/container-version-check`
- bumps `docs.current-version` in `service-versions.yaml` to the release tag
## Cutover plan (manual, from gilbert, after review)
1. **Take down old:**
- Remove the cv and docs Applications: `argocd app delete cv --cascade && argocd app delete docs --cascade`
- Verify k8s namespaces gone: `kubectl --context=minikube-indri get ns | grep -E '^(cv|docs)\\b'` (should be empty)
- Verify tailnet MagicDNS no longer advertises the VIPs: `nslookup cv.tail8d86e.ts.net` and `nslookup docs.tail8d86e.ts.net` should both fail
2. **Bring up new:**
- `mise run provision-indri -- --tags cv,docs,caddy --check --diff` (already validated on branch)
- `mise run provision-indri -- --tags cv,docs,caddy`
- `fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"`
3. **Verify:** `mise run services-check` and the curl checks listed in `docs/how-to/operations/{cv,docs}-on-indri.md`
4. **Cleanup commit + merge.**
Total expected downtime: minutes (not the few-hour budget you authorized).
## Test plan
- [ ] `mise run provision-indri -- --tags cv,docs --check --diff` clean
- [ ] `mise run provision-indri -- --tags caddy --check --diff` shows only the cv + docs blocks changing as previewed in the PR thread
- [ ] After cutover: `cv.eblu.me`, `cv.ops.eblu.me`, `docs.eblu.me`, `docs.ops.eblu.me` all return 200
- [ ] `cv.eblu.me/resume.pdf` includes `Content-Disposition: attachment`
- [ ] A clean Quartz URL (e.g. `docs.eblu.me/explanation/agent-change-process`) resolves to the right page
- [ ] `mise run services-check` clean
- [ ] `mise run service-review --type ansible` shows cv and docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/342
2026-04-29 14:55:11 -07:00
|
|
|
kind: static
|
|
|
|
|
root: "{{ docs_content_dir }}"
|
|
|
|
|
try_html: true # Quartz: path → path/ → path.html → 404.html
|
2026-02-12 11:09:41 -08:00
|
|
|
- name: cv
|
|
|
|
|
host: "cv.{{ caddy_domain }}"
|
C1: migrate cv + docs from minikube to indri-native (#342)
## Summary
Replace the cv (`cv.eblu.me`) and docs (`docs.eblu.me`) minikube Deployments with indri-native ansible roles. Caddy serves the extracted release tarballs directly via a new `kind: static` service-block — no daemon, no nginx pod, no ProxyGroup ingress on the request path. Mirrors the rationale of the recent devpi migration; part of the broader minikube wind-down.
## What's in this commit
- `ansible/roles/{cv,docs}` — sentinel-gated tarball download + extract into `~/{cv,docs}/content/`
- `ansible/roles/caddy/` — new `kind: static` branch in the Caddyfile template (encoded gzip, immutable cache headers for fingerprinted assets, optional `try_html` for Quartz-style clean URLs, optional per-path `download_paths` for the resume PDF's `Content-Disposition`)
- `ansible/playbooks/indri.yml` — wires `cv` and `docs` roles before `caddy`
- `service-versions.yaml` — both services flip to `type: ansible`. `docs.current-version` stays at `1.28.2` for this commit so `container-version-check` keeps passing while `containers/quartz/Dockerfile` still exists; it moves to the docs release tag in the cleanup commit
- `.forgejo/workflows/{cv-deploy,build-blumeops}.yaml` — deploy step now bumps `cv_version`/`docs_version` in the role defaults and pushes; running ansible + purging the Fly cache is manual from gilbert (matches devpi)
- Docs: `docs/how-to/operations/{cv,docs}-on-indri.md`, updated `docs/reference/services/{cv,docs}.md`, changelog fragment
## What is not in this commit
The dead artifacts. After PR review and successful cutover, a follow-up commit deletes:
- `argocd/apps/{cv,docs}.yaml` and `argocd/manifests/{cv,docs}/`
- `containers/cv/`, `containers/quartz/`
- `CONTAINER_TO_SERVICE['quartz']` mapping in `mise-tasks/container-version-check`
- bumps `docs.current-version` in `service-versions.yaml` to the release tag
## Cutover plan (manual, from gilbert, after review)
1. **Take down old:**
- Remove the cv and docs Applications: `argocd app delete cv --cascade && argocd app delete docs --cascade`
- Verify k8s namespaces gone: `kubectl --context=minikube-indri get ns | grep -E '^(cv|docs)\\b'` (should be empty)
- Verify tailnet MagicDNS no longer advertises the VIPs: `nslookup cv.tail8d86e.ts.net` and `nslookup docs.tail8d86e.ts.net` should both fail
2. **Bring up new:**
- `mise run provision-indri -- --tags cv,docs,caddy --check --diff` (already validated on branch)
- `mise run provision-indri -- --tags cv,docs,caddy`
- `fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"`
3. **Verify:** `mise run services-check` and the curl checks listed in `docs/how-to/operations/{cv,docs}-on-indri.md`
4. **Cleanup commit + merge.**
Total expected downtime: minutes (not the few-hour budget you authorized).
## Test plan
- [ ] `mise run provision-indri -- --tags cv,docs --check --diff` clean
- [ ] `mise run provision-indri -- --tags caddy --check --diff` shows only the cv + docs blocks changing as previewed in the PR thread
- [ ] After cutover: `cv.eblu.me`, `cv.ops.eblu.me`, `docs.eblu.me`, `docs.ops.eblu.me` all return 200
- [ ] `cv.eblu.me/resume.pdf` includes `Content-Disposition: attachment`
- [ ] A clean Quartz URL (e.g. `docs.eblu.me/explanation/agent-change-process`) resolves to the right page
- [ ] `mise run services-check` clean
- [ ] `mise run service-review --type ansible` shows cv and docs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/342
2026-04-29 14:55:11 -07:00
|
|
|
kind: static
|
|
|
|
|
root: "{{ cv_content_dir }}"
|
|
|
|
|
download_paths:
|
|
|
|
|
- path: /resume.pdf
|
|
|
|
|
filename: erich-blume-resume.pdf
|
Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify (#190)
## Summary
Deploy a cloud-free NVR stack for the GableCam (ReoLink Elite Floodlight at 192.168.1.159):
- **Mosquitto** — shared MQTT broker in `mqtt` namespace (cluster-internal, no auth)
- **Ntfy** — self-hosted push notifications in `ntfy` namespace, exposed at `ntfy.tail8d86e.ts.net` / `ntfy.ops.eblu.me`
- **Frigate** — NVR with GableCam via HTTP-FLV, ONNX CPU detection, NFS recordings on sifaka, exposed at `nvr.tail8d86e.ts.net` / `nvr.ops.eblu.me`
- **frigate-notify** — bridges Frigate detection events (person, car, dog, cat) to Ntfy alerts via MQTT
Also includes:
- Prometheus scrape target for Frigate metrics
- Grafana dashboard for Frigate (status, inference speed, FPS, CPU/memory, storage)
- Caddy reverse proxy entries for `nvr.ops.eblu.me` and `ntfy.ops.eblu.me`
## Prerequisites
- [ ] Create NFS share `frigate` on sifaka (`/volume1/frigate`, RW for indri)
- [ ] Create 1Password item "Reolink Floodlight Camera" in `blumeops` vault with `username` and `password` fields
## Deployment (after merge)
```bash
argocd app sync apps
argocd app sync mosquitto
argocd app sync ntfy
argocd app sync frigate
argocd app sync grafana-config
argocd app sync prometheus
mise run provision-indri -- --tags caddy
mise run services-check
```
## Verification
- [ ] Mosquitto pod running, accepting connections on 1883
- [ ] Ntfy web UI accessible at `ntfy.ops.eblu.me`
- [ ] Frigate web UI at `nvr.ops.eblu.me` showing GableCam live feed
- [ ] Object detection working (ONNX, person/car/dog/cat)
- [ ] Recordings appearing in NFS share on sifaka
- [ ] frigate-notify sending detection alerts to Ntfy
- [ ] Prometheus scraping Frigate metrics
- [ ] Grafana dashboard showing Frigate data
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/190
2026-02-14 21:27:44 -08:00
|
|
|
- name: nvr
|
|
|
|
|
host: "nvr.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://nvr.tail8d86e.ts.net"
|
2026-02-20 12:55:59 -08:00
|
|
|
- name: authentik
|
|
|
|
|
host: "authentik.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://authentik.tail8d86e.ts.net"
|
2026-04-01 21:37:57 -07:00
|
|
|
cache_policy: spa
|
Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify (#190)
## Summary
Deploy a cloud-free NVR stack for the GableCam (ReoLink Elite Floodlight at 192.168.1.159):
- **Mosquitto** — shared MQTT broker in `mqtt` namespace (cluster-internal, no auth)
- **Ntfy** — self-hosted push notifications in `ntfy` namespace, exposed at `ntfy.tail8d86e.ts.net` / `ntfy.ops.eblu.me`
- **Frigate** — NVR with GableCam via HTTP-FLV, ONNX CPU detection, NFS recordings on sifaka, exposed at `nvr.tail8d86e.ts.net` / `nvr.ops.eblu.me`
- **frigate-notify** — bridges Frigate detection events (person, car, dog, cat) to Ntfy alerts via MQTT
Also includes:
- Prometheus scrape target for Frigate metrics
- Grafana dashboard for Frigate (status, inference speed, FPS, CPU/memory, storage)
- Caddy reverse proxy entries for `nvr.ops.eblu.me` and `ntfy.ops.eblu.me`
## Prerequisites
- [ ] Create NFS share `frigate` on sifaka (`/volume1/frigate`, RW for indri)
- [ ] Create 1Password item "Reolink Floodlight Camera" in `blumeops` vault with `username` and `password` fields
## Deployment (after merge)
```bash
argocd app sync apps
argocd app sync mosquitto
argocd app sync ntfy
argocd app sync frigate
argocd app sync grafana-config
argocd app sync prometheus
mise run provision-indri -- --tags caddy
mise run services-check
```
## Verification
- [ ] Mosquitto pod running, accepting connections on 1883
- [ ] Ntfy web UI accessible at `ntfy.ops.eblu.me`
- [ ] Frigate web UI at `nvr.ops.eblu.me` showing GableCam live feed
- [ ] Object detection working (ONNX, person/car/dog/cat)
- [ ] Recordings appearing in NFS share on sifaka
- [ ] frigate-notify sending detection alerts to Ntfy
- [ ] Prometheus scraping Frigate metrics
- [ ] Grafana dashboard showing Frigate data
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/190
2026-02-14 21:27:44 -08:00
|
|
|
- name: ntfy
|
|
|
|
|
host: "ntfy.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://ntfy.tail8d86e.ts.net"
|
2026-03-02 20:39:51 -08:00
|
|
|
- name: ollama
|
|
|
|
|
host: "ollama.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://ollama.tail8d86e.ts.net"
|
2026-03-16 21:59:10 -07:00
|
|
|
- name: mealie
|
|
|
|
|
host: "meals.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://meals.tail8d86e.ts.net"
|
2026-04-08 17:54:12 -07:00
|
|
|
- name: paperless
|
|
|
|
|
host: "paperless.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://paperless.tail8d86e.ts.net"
|
C1: deploy adelaide-baby-shower-app to ringtail k3s (#349)
## Summary
Brings up the Adelaide / Heidi / Addie baby shower app on ringtail k3s with the public/private split that the app's hosting contract calls for: `shower.eblu.me` (public, via Fly proxy) and `shower.ops.eblu.me` (tailnet). App is consumed as a wheel from the Forgejo PyPI index — source lives at [`adelaide-baby-shower-app`](https://forge.eblu.me/eblume/adelaide-baby-shower-app).
### What's included
- **ArgoCD app + manifests** under `argocd/manifests/shower/` (deployment, service, ProxyGroup ingress, ConfigMap for `DJANGO_DEBUG`/`DJANGO_ADMIN_URL`, ExternalSecret for `DJANGO_SECRET_KEY` from 1Password item `Shower (blumeops)`, NFS PV on sifaka, RWX media PVC, RWO local-path data PVC for SQLite). Recreate rollout because SQLite is single-writer.
- **Public surface** (`fly/`): new `shower.eblu.me` server block proxying to `shower.ops.eblu.me`. `/admin/` returns 403 at the edge except `/admin/login/` and `/admin/logout/`, which are rate-limited via a new `shower_auth` zone. `X-Clacks-Overhead` on. GNU Terry Pratchett.
- **fail2ban** filter (`shower-admin-login.conf`) matching 401/403/429 on `/admin/login/` and jail (`shower.conf`) with `maxretry=5/findtime=600/bantime=3600`. The `nginx-deny` action was generalized to take a per-jail `nginx_deny_file` so the shower has its own deny list (forge keeps using the legacy default).
- **Caddy** route on indri (`shower.ops.eblu.me` → `https://shower.tail8d86e.ts.net`).
- **Pulumi** Gandi CNAME `shower.eblu.me → blumeops-proxy.fly.dev.`.
- **Grafana** APM dashboard `configmap-shower-apm.yaml` (request rate, error rate, failed admin login count, latency percentiles, bandwidth, access logs) mirroring `docs-apm.json` with a `host="shower.eblu.me"` filter.
- **Container** `containers/shower/default.nix` — `dockerTools.buildLayeredImage` with a nixpkgs Python and a startup wrapper that creates `/app/data/.venv`, pip-installs `adelaide-baby-shower-app==1.0.0` from the forge PyPI index on first boot, runs migrations + collectstatic, and execs gunicorn. A `local_settings.py` shim pins `DATABASES.NAME`/`MEDIA_ROOT`/`STATIC_ROOT` to absolute paths so they don't end up in site-packages.
- **Docs** runbook at `docs/how-to/operations/shower-app.md` linked from the apps registry, plus changelog fragments.
### Defense layers on the public surface
1. fly nginx geo+fail2ban `$shower_banned` (per-service deny list)
2. fly nginx `limit_req zone=shower_auth` (3 r/s per Fly-Client-IP)
3. django-axes (5 fails / 1h, keyed on username+ip_address)
4. edge `/admin/` block (returns 403 for anything that isn't login/logout)
## Prerequisites for the user to do (NOT in this PR)
Halted on these per request — they touch shared/manual systems:
- [x] **NFS share** on sifaka: `/volume1/shower`, NFS rule for ringtail RW, `chown 1000:1000`
- [ ] **1Password item** `Shower (blumeops)` in the blumeops vault with a freshly minted `secret-key` field (`openssl rand -base64 48`) — do NOT reuse anything that has lived in git
- [ ] **Container build**: `mise run container-build-and-release shower`, then update `images[].newTag` in `argocd/manifests/shower/kustomization.yaml` to the resulting `v1.0.0-<sha>-nix`
- [x] **DNS**: `mise run dns-up` after merge
- [x] **Fly cert**: `fly certs add shower.eblu.me -a blumeops-proxy`
- [ ] **Caddy push**: `mise run provision-indri -- --tags caddy`
- [ ] **Fly redeploy** to pick up the new nginx block + fail2ban jail: `mise run fly-deploy`
- [ ] **ArgoCD sync**: `argocd app set shower --revision shower-app-deploy && argocd app sync shower` to test from this branch before merging
## Test plan
- [ ] Container builds successfully on nix-container-builder runner
- [ ] Pod starts, migrations run, gunicorn answers on :8000
- [ ] `kubectl --context=k3s-ringtail -n shower logs deploy/shower` clean
- [ ] `curl -sf https://shower.ops.eblu.me/` returns the splash page (tailnet)
- [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/` returns 200 (pre-DNS verification)
- [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/admin/users/` returns 403 (edge block)
- [ ] `curl -I -H "Host: shower.eblu.me" https://blumeops-proxy.fly.dev/admin/login/` returns a Django login response
- [ ] After DNS is up: `curl -I https://shower.eblu.me/` returns 200 with `X-Clacks-Overhead`
- [ ] Grafana dashboard "Shower APM" appears and starts showing traffic
- [ ] `mise run services-check` passes
Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/349
2026-05-11 13:47:18 -07:00
|
|
|
- name: shower
|
|
|
|
|
host: "shower.{{ caddy_domain }}"
|
|
|
|
|
backend: "https://shower.tail8d86e.ts.net"
|
Add Hajimari service dashboard (#73)
## Summary
- Add Hajimari as a service dashboard/start page at `go.ops.eblu.me`
- Auto-discovers k8s services from ingress annotations
- Custom apps for non-k8s services: Forgejo, Registry, Sifaka NAS
- Add `nas.ops.eblu.me` Caddy proxy to Synology dashboard
## Services Configured
**Auto-discovered (k8s ingresses with hajimari.io annotations):**
- Grafana, ArgoCD, Prometheus, Loki (Observability)
- Miniflux, Kiwix, Transmission, TeslaMate, Immich (Apps)
- PyPI/devpi (Infrastructure)
**Custom apps (non-k8s):**
- Forgejo (forge.ops.eblu.me)
- Registry (registry.ops.eblu.me)
- Sifaka NAS (nas.ops.eblu.me)
**Bookmarks:**
- Tailscale Admin, 1Password, Pulumi
## Deployment and Testing
- [ ] Sync `apps` application to pick up new Hajimari Application
- [ ] Sync `hajimari` application
- [ ] Run `mise run provision-indri -- --tags caddy` for go/nas proxy entries
- [ ] Re-sync all k8s apps with hajimari annotations (or wait for natural drift)
- [ ] Verify https://go.ops.eblu.me shows dashboard with all services
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/73
2026-01-29 15:51:42 -08:00
|
|
|
- name: sifaka
|
|
|
|
|
host: "nas.{{ caddy_domain }}"
|
|
|
|
|
backend: "http://sifaka:5000"
|
2026-01-25 11:37:23 -08:00
|
|
|
|
2026-01-25 12:56:31 -08:00
|
|
|
# Layer 4 (TCP) services
|
2026-01-25 11:37:23 -08:00
|
|
|
# Format: { port: external_port, backend: "host:port" }
|
2026-01-25 12:56:31 -08:00
|
|
|
caddy_tcp_services:
|
2026-01-25 11:37:23 -08:00
|
|
|
- port: 2222
|
|
|
|
|
backend: "localhost:2200" # Forgejo SSH
|
2026-01-25 12:56:31 -08:00
|
|
|
- port: 5432
|
2026-03-27 16:59:58 -07:00
|
|
|
backend: "pg.tail8d86e.ts.net:5432" # PostgreSQL (blumeops-pg)
|
|
|
|
|
- port: 5433
|
|
|
|
|
backend: "immich-pg.tail8d86e.ts.net:5432" # PostgreSQL (immich-pg)
|
2026-02-09 17:44:05 -08:00
|
|
|
- port: "{{ sifaka_node_exporter_port }}"
|
|
|
|
|
backend: "sifaka:{{ sifaka_node_exporter_port }}" # Sifaka node_exporter
|
|
|
|
|
- port: "{{ sifaka_smartctl_exporter_port }}"
|
|
|
|
|
backend: "sifaka:{{ sifaka_smartctl_exporter_port }}" # Sifaka smartctl_exporter
|