docs(vault): fleet-wide fact re-verification pass (67 pages)
The docs-fact census wave re-read the live hosts and re-keyed every page it could: runtime/payload identities, ports, sizes, ledger figures, unit state, and the retirement banners that drifted as the fleet moved. The wave was interrupted mid-run, so this commit carries only what passed the integrity gates: every touched page parses (front matter or H1+status convention intact, balanced fences, no truncated tables, trailing newline), the docs_completeness and link-integrity seams are green, and bats is 384/384. Numbers that this session proved on live hosts are keyed to the measured value, not the page's earlier claim.
This commit is contained in:
1 parent
466fd477d8
commit
e7f0d93c86
67 files changed
+2619
-1152
No files matched your search
@@ -17,8 +17,10 @@ timestamp: 2026-09-12
|
||||
Marreta is a self-hosted **content-cleaning proxy**: you hand it a URL and it
|
||||
returns a stripped, readable version of the page — no paywall chrome, no
|
||||
tracking, no cookie walls. It is a Laravel 13 app on the
|
||||
`serversideup/php:8.4-fpm-nginx` image, with an optional headless-browser
|
||||
companion for JavaScript-heavy pages.
|
||||
`serversideup/php:8.4-fpm-nginx` image (`composer.json` requires
|
||||
`php: ^8.4`, `laravel/framework: ^13.0`, `filament/filament: ^5.7` —
|
||||
<https://raw.githubusercontent.com/manualdousuario/marreta/main/composer.json>),
|
||||
with an optional headless-browser companion for JavaScript-heavy pages.
|
||||
|
||||
**Host:** Dankclaw — **vhost:** `marreta.franksdank.cloud` (Caddy, guard-chained)
|
||||
— **Upstream:** host-published port `8082` (inside the Gluetun netns)
|
||||
@@ -28,11 +30,16 @@ companion for JavaScript-heavy pages.
|
||||
|
||||
- Fetches a target URL with a cascade of strategies: plain HTTP (curl), a
|
||||
headless browser (Lightpanda) for JS pages, and the Internet Archive
|
||||
(Wayback) as a last resort.
|
||||
(Wayback) as a last resort — the three `app/Services/Fetch/*FetchStrategy.php`
|
||||
files upstream (`BrowserFetchStrategy`, `CurlFetchStrategy`,
|
||||
`WaybackFetchStrategy`, dispatched by `app/Services/FetchManager.php`).
|
||||
- Strips paywall/consent/tracking markup and re-renders clean HTML.
|
||||
- Ships a Filament admin panel at `/admin` (login = `ADMIN_EMAIL` /
|
||||
`ADMIN_PASSWORD`) for settings and usage.
|
||||
- Health endpoint at `/up` (Laravel's built-in health route).
|
||||
`ADMIN_PASSWORD`) for settings and usage — live 2026-10-18
|
||||
`curl http://127.0.0.1:8082/admin/login` → **200**.
|
||||
- Health endpoint at `/up` (Laravel's built-in health route —
|
||||
`bootstrap/app.php` passes `health: '/up'`). Live: `curl
|
||||
http://127.0.0.1:8082/up` → **200** (2026-10-18).
|
||||
|
||||
## Topology
|
||||
|
||||
@@ -58,11 +65,17 @@ graph LR
|
||||
|
||||
| Unit | File | Image | Netns | Listens |
|
||||
|------|------|-------|-------|---------|
|
||||
| `marreta.service` | `dankclaw/marreta/quadlet/marreta.container` | `ghcr.io/manualdousuario/marreta:latest` | `gluetun.container` | `8085` (nginx) |
|
||||
| `marreta-browser.service` | `dankclaw/marreta/quadlet/marreta-browser.container` | `docker.io/lightpanda/browser:nightly` | `gluetun.container` | `9222` (CDP) |
|
||||
| `marreta.service` | `dankclaw/marreta/quadlet/marreta.container` | `ghcr.io/manualdousuario/marreta:latest` (`:35`) | `gluetun.container` (`:34`) | `8085` (nginx, `NGINX_HTTP_PORT=8085`, `:37`) |
|
||||
| `marreta-browser.service` | `dankclaw/marreta/quadlet/marreta-browser.container` | `docker.io/lightpanda/browser:nightly` (`:26`) | `gluetun.container` (`:25`) | `9222` (CDP) |
|
||||
|
||||
Both units are boot-persistent (`WantedBy=default.target`) and self-healing
|
||||
(`Restart=always`), per the repo's Boot Persistence rule.
|
||||
(`Restart=always`, `RestartSec=10` — `marreta.container:62-69`,
|
||||
`marreta-browser.container:30-34`), per the repo's Boot Persistence rule. Live
|
||||
2026-10-18 both report `systemctl --user is-enabled` → `generated` and
|
||||
`is-active` → `active`, and `podman inspect` reports marreta **healthy**
|
||||
(`HealthCmd` GETs `http://127.0.0.1:8085/up`, `marreta.container:53`) and
|
||||
marreta_browser `running`. Both images are `:latest`, so a redeploy can move
|
||||
them.
|
||||
|
||||
## The port-collision resolution (why 8085)
|
||||
|
||||
@@ -81,8 +94,11 @@ The fix is a single env var, verified against the serversideup source:
|
||||
port off the collision with no custom build.
|
||||
|
||||
Marreta then publishes **nothing** of its own — the netns owner (Gluetun)
|
||||
publishes `8082:8085` on its behalf, exactly as it publishes `8081:8080`
|
||||
for SearXNG. Caddy dials `192.168.0.143:8082`.
|
||||
publishes `8082:8085` on its behalf (`dankclaw/searxng/quadlet/gluetun.container:32`),
|
||||
exactly as it publishes `8081:8080` for SearXNG. Caddy dials
|
||||
`192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`). Live env confirms the
|
||||
relocation: `podman inspect marreta` carries `NGINX_HTTP_PORT=8085`
|
||||
(2026-10-18).
|
||||
|
||||
## Why netns sharing, not `PROXY_URL`
|
||||
|
||||
@@ -93,7 +109,13 @@ browser ran outside the tunnel it would leak the host's real egress IP on
|
||||
every JS-rendered page. Netns sharing is the only way to put **both** the
|
||||
app and the browser on the same exit, which is what "share the same Gluetun
|
||||
egress as SearXNG" requires. The browser is therefore reached on loopback
|
||||
(`ws://127.0.0.1:9222`) inside the shared netns.
|
||||
(`ws://127.0.0.1:9222`) inside the shared netns — upstream's own compose uses
|
||||
`ws://marreta_browser:9222`, which needs no service name here because both
|
||||
containers share one namespace. Live env: `BROWSER_WS_ENDPOINT=ws://127.0.0.1:9222`
|
||||
(`podman inspect marreta` 2026-10-18; `marreta.container:44`). Upstream agrees
|
||||
on the mechanism: `BrowserFetchStrategy.php` only calls
|
||||
`BrowserFactory::connectToBrowser($wsEndpoint)` and never passes a proxy, while
|
||||
`CurlFetchStrategy.php:24-25` is the only place `marreta.proxy_url` is read.
|
||||
|
||||
## Guard chain (behind Authentik)
|
||||
|
||||
@@ -101,16 +123,19 @@ egress as SearXNG" requires. The browser is therefore reached on loopback
|
||||
[[Anubis]] and [[Authentik]]:
|
||||
|
||||
1. **Public edge** — the vhost proxies everything to `anubis:8923`
|
||||
(bot challenge), with `header_up X-Real-Ip {remote_host}`.
|
||||
(bot challenge), with `header_up X-Real-Ip {remote_host}`
|
||||
(`dankclaw/Caddyfile:159-163`).
|
||||
2. **Auth decision** — Anubis dials Caddy's `http://:8016`, which proxies
|
||||
whole-request to `authentik-server:9000`.
|
||||
whole-request to `authentik-server:9000` (`dankclaw/Caddyfile:22-24,94`).
|
||||
3. **Post-auth routing** — authenticated traffic returns to `http://:8017`,
|
||||
where `@marreta host marreta.franksdank.cloud` routes to
|
||||
`reverse_proxy 192.168.0.143:8082`.
|
||||
`reverse_proxy 192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`).
|
||||
|
||||
**Access model: any authenticated user.** The post-auth handler carries no
|
||||
group gate (unlike the homepage `/admin`, which requires the
|
||||
`homepage-admins` group). Anyone who can log in via Authentik can use
|
||||
`homepage-admins` group) — the Caddyfile says so in the block header
|
||||
("Any authentik-authenticated user may use it (no group gate)",
|
||||
`dankclaw/Caddyfile:156-157`). Anyone who can log in via Authentik can use
|
||||
Marreta.
|
||||
|
||||
**Required provider (manual, one-time).** Before the vhost goes live, a
|
||||
@@ -136,8 +161,12 @@ must change together.
|
||||
## Scheme / proxy trust
|
||||
|
||||
Marreta ships `trustProxies(at: '172.16.0.0/12,10.0.0.0/8,192.168.0.0/16,127.0.0.1')`
|
||||
and forces `https` when `APP_URL` starts with `https://`. With
|
||||
`APP_URL=https://marreta.franksdank.cloud` the app honors the forwarded
|
||||
— that is the `TRUSTED_PROXIES` fallback in upstream `bootstrap/app.php`, and
|
||||
this container sets no `TRUSTED_PROXIES`, so the default is what runs
|
||||
(`podman inspect marreta` 2026-10-18) — and forces `https` when `APP_URL`
|
||||
starts with `https://` (`app/Providers/AppServiceProvider.php:23-24`,
|
||||
`URL::forceScheme('https')`). With
|
||||
`APP_URL=https://marreta.franksdank.cloud` (`dankclaw/marreta/quadlet/marreta.container:41`) the app honors the forwarded
|
||||
scheme and client IP through the whole chain, so there is no redirect loop
|
||||
or mixed-content breakage.
|
||||
|
||||
@@ -145,19 +174,24 @@ or mixed-content breakage.
|
||||
|
||||
Three values, seeded into `/home/dank/Containers/marreta/env` (mode 0600,
|
||||
dank-owned so the rootless unit can read it — the grafana convention, not
|
||||
`/opt`):
|
||||
`/opt`; live 2026-10-18 `-rw------- dank dank` 155 B). The seeder writes it
|
||||
only when the file does not exist and `chmod 600`s it
|
||||
(`seed_marreta_env`, `scripts/deploy-remote.sh:615-622`).
|
||||
|
||||
| Repo `.env` key | Mapped to (app) | Notes |
|
||||
|-----------------|-----------------|-------|
|
||||
| `MARRETA_APP_KEY` | `APP_KEY` | Laravel encryption key. **Must be the `base64:`-prefixed form**: `base64:$(openssl rand -base64 32)`. A bare base64 string (no prefix) is read as a raw key of the wrong length and every app route 500s with `Unsupported cipher or incorrect key length` at `Encrypter.php:61` (only `/up`, which skips the encrypter, still passes). Changing it after first boot invalidates encrypted session/cookie data. |
|
||||
| `MARRETA_APP_KEY` | `APP_KEY` | Laravel encryption key. **Must be the `base64:`-prefixed form**: `base64:$(openssl rand -base64 32)`. A bare base64 string (no prefix) is read as a raw key of the wrong length and every app route 500s with `Unsupported cipher or incorrect key length` at `Encrypter.php:61` (laravel/framework 13.x, `src/Illuminate/Encryption/Encrypter.php:61`) while `/up`, which skips the encrypter, still passes. Today's key is well-formed: `/`, `/admin/login` and `/up` all answer 200 (`curl http://127.0.0.1:8082/…` 2026-10-18). Changing it after first boot invalidates encrypted session/cookie data. |
|
||||
| `MARRETA_ADMIN_EMAIL` | `ADMIN_EMAIL` | Admin panel login (created on first boot). |
|
||||
| `MARRETA_ADMIN_PASSWORD` | `ADMIN_PASSWORD` | Admin panel password. |
|
||||
|
||||
The quadlet maps them with `Environment=APP_KEY=${MARRETA_APP_KEY}` etc.
|
||||
The `.container` files hold **no** secrets — only `${VAR}` placeholders.
|
||||
(`dankclaw/marreta/quadlet/marreta.container:47-49`), loaded from the env file
|
||||
via `[Service] EnvironmentFile=` (`:60`). The `.container` files hold **no**
|
||||
secrets — only `${VAR}` placeholders.
|
||||
The SQLite DB + cache live in the `marreta_storage` named volume at
|
||||
`/var/www/html/storage/app` (podman auto-creates it and copyups the image's
|
||||
`www-data` ownership so the app can write).
|
||||
`/var/www/html/storage/app` (`marreta.container:52`; podman auto-creates it and
|
||||
copyups the image's `www-data` ownership so the app can write). Live 2026-10-18
|
||||
the volume exists at `/home/dank/.local/share/containers/storage/volumes/marreta_storage/_data`.
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -165,11 +199,11 @@ The SQLite DB + cache live in the `marreta_storage` named volume at
|
||||
./scripts/deploy.sh dankclaw --service marreta
|
||||
```
|
||||
|
||||
`deploy_marreta` seeds the env file (only if absent), installs both
|
||||
`deploy_marreta` (`scripts/deploy-remote.sh:630-640`) seeds the env file (only if absent), installs both
|
||||
`.container` units, ensures Gluetun is up, then restarts the browser before
|
||||
the app. Because Marreta shares Gluetun's netns, the planner treats it as
|
||||
part of the Gluetun netns family: a change to `gluetun.container` or
|
||||
`searxng.network` also selects `marreta` (a Gluetun recreation tears the
|
||||
`searxng.network` also selects `marreta` (`scripts/deploy-plan.py:295-309` — a Gluetun recreation tears the
|
||||
netns down and would otherwise orphan it).
|
||||
|
||||
> **First deploy note:** adding `PublishPort=8082:8085` to `gluetun.container`
|
||||
@@ -180,14 +214,20 @@ netns down and would otherwise orphan it).
|
||||
|
||||
- **Unauthenticated:** `curl -s https://marreta.franksdank.cloud/` → the
|
||||
Anubis challenge page (grep body for `challenge`/`not a bot`). A 502 here
|
||||
means the chain is mis-wired.
|
||||
means the chain is mis-wired. Re-checked 2026-10-18: **200** with `challenge`
|
||||
×5 and `not a bot` ×2 in the body — the guard chain is live.
|
||||
- **Post-auth routing:** `podman exec caddy sh -c "grep -n -A3 '@marreta host' /etc/caddy/Caddyfile"`
|
||||
after the deploy's reload line.
|
||||
after the deploy's reload line (repo copy: `dankclaw/Caddyfile:419-422`).
|
||||
- **Egress:** from inside the netns, `podman exec marreta curl -s https://ifconfig.co`
|
||||
must return the VPN exit IP, not the home IP.
|
||||
must return the VPN exit IP, not the home IP. The tunnel's own egress is
|
||||
readable without exec: `journalctl --user -u gluetun` logged exit
|
||||
`149.22.88.19` (United States, Washington, Seattle) on 2026-10-18.
|
||||
- **Port:** `podman exec marreta sh -c 'curl -fsS localhost:8085/up'` → 200;
|
||||
`curl http://192.168.0.143:8082/up` → 200.
|
||||
- **Test seam:** `bats tests/marreta_quadlet.bats` (17 contracts).
|
||||
`curl http://192.168.0.143:8082/up` → 200 (also **200** from the host
|
||||
loopback, `curl http://127.0.0.1:8082/up` 2026-10-18; the container health
|
||||
probe GETs `http://127.0.0.1:8085/up` and reports **healthy**).
|
||||
- **Test seam:** `bats tests/marreta_quadlet.bats` (17 contracts — 17 `@test`
|
||||
blocks, counted 2026-10-18).
|
||||
|
||||
### Live verification (deployed 2026-09-12, rev `91e9410`)
|
||||
|
||||
|
||||
Reference in new issue
Block a user