Files
serverAppConfigs/serverAppConfigs-docs/Dankclaw/Marreta.md
T
Frank a5fe045b58 docs(vault): one fact, one owner — repair the doubled pages, single-source the facts
Structural repair:
- 13 pages were the same document twice (each exactly 2x its HEAD size),
  a second frontmatter block whose closing fence had been glued onto a body
  line by a frontmatter rewrite that re-read its own output. Un-doubled by
  cutting at the second frontmatter and keeping the copy whose body is a
  superset; the orphan census reports 0 lost tokens against HEAD.
- log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first —
  66 dated headings merged into 34, titles demoted to bold leads, every
  content line verified present. The file had been descending at the head
  and ascending at the tail.

Ownership moves:
- New `LLM/frank-medium.md` is the locator for the retired Dankserver
  endpoint: era windows quoted from each era page's own banner, no numbers
  restated, and the two unresolved date conflicts recorded instead of
  silently resolved. `frank-archon.md` links to it instead of restating the
  pre-rename window.
- The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone;
  `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor
  verdicts and cross-split spec decode, and the four links that pointed at
  the stubs were repointed at it or at the drafter page.
- Authentik's three stub pages fold into `Authentik.md`, with the process
  detail and the outpost reference split out so the parent stays under the
  length hint.

Verified corrections (host census + live probe, 2026-10-19):
- The MXFP4 checkpoint two pages called the "on-disk rollback" is gone:
  /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on
  the era page that owns that payload; the swap page now links to it.
- frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2
  arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run
  census is kept once (155 requests) instead of twice (~120 and 155).
- Fleet shapes re-confirmed against the live endpoints before touching any
  page: 3x200000 fp8 KV, 2x200704, 2x262144.

Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
2026-10-19 05:00:43 -07:00

14 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
reference Marreta on Dankclaw The content-cleaning proxy SearXNG borrows egress from: a quadlet pair in the VPN netns behind the guard chain.
dankclaw
marreta
proxy
gluetun
authentik
quadlet
2026-09-12

Marreta

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 (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) — Source: https://github.com/manualdousuario/marreta

What it does (from the upstream README)

  • 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 — 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 — 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

Marreta runs as two rootless Quadlet containers that both join Gluetun's network namespace — the same tunnel SearXNG uses — so every outbound fetch exits over WireGuard, never the home IP.

graph LR
    C[Client] -->|HTTPS| TLS[Caddy TLS edge]
    TLS -->|challenge| AN[Anubis :8923]
    AN -->|auth decision| A16[Caddy :8016]
    A16 -->|Authentik| AUTH[authentik-server]
    AUTH -->|authenticated| A17[Caddy :8017]
    A17 -->|host marreta| PUB[host :8082]
    subgraph G[gluetun netns - WireGuard egress]
        PUB -->|8085| M[marreta :8085]
        M -->|ws loopback| B[marreta_browser :9222]
    end
    M -->|fetch| WG[WireGuard tunnel]
    B -->|fetch| WG
Unit File Image Netns Listens
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, 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)

The shared netns already has SearXNG on 8080. Marreta's image (serversideup/php:8.4-fpm-nginx) defaults to NGINX_HTTP_PORT=8080, so a naive deploy would fail the moment nginx tried to bind a port SearXNG holds.

The fix is a single env var, verified against the serversideup source:

  • The image sets ENV NGINX_HTTP_PORT="8080" and the site template is listen ${NGINX_HTTP_PORT} default_server;.
  • The image does not bake conf.d/*.conf (it rm -rfs them at build); the entrypoint 10-init-webserver-config.sh runs envsubst over every NGINX_* variable at container start.
  • So Environment=NGINX_HTTP_PORT=8085 in the quadlet relocates the listen 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 (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

Marreta supports a PROXY_URL env var, but it only covers the curl fetch strategy. The BrowserFetchStrategy connects to Lightpanda over CDP and passes no proxy — the browser makes its own connections. If the 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 — 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)

marreta.franksdank.cloud follows the standard chain documented in Anubis and Authentik:

  1. Public edge — the vhost proxies everything to anubis:8923 (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 (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 (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) — 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 proxy provider must exist on the embedded outpost or authenticated traffic hits the outpost error page instead of the app:

# podman exec -i authentik-server ak shell   < provider.py
from authentik.core.models import Group
from authentik.providers.proxy.models import ProxyProvider

p = ProxyProvider.objects.filter(name="marreta").first() or ProxyProvider()
p.name = "marreta"
p.external_host = "https://marreta.franksdank.cloud/"   # exact, trailing slash
p.internal_host = "http://caddy:8017"
p.save()

This is the "provider first, then route" ordering from the guard-chain runbook — the Anubis guard set, the Caddy TLS edge, and the provider list 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') — 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.

Secrets

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; live 2026-10-18 -rw------- dank dank 155 B). The seeder writes it only when the file does not exist and chmod 600s 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 (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. (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 (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

./scripts/deploy.sh dankclaw --service marreta

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 (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 means the first deploy recreates Gluetun, which briefly cycles SearXNG and Marreta together. Plan for the short tunnel restart.

Verification

  • 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. 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 (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. 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 (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)

Deployed via deploy.sh dankclaw --service gluetun --service marreta --service caddy (gluetun first to recreate the tunnel with the new 8082:8085 publish, which also cycles SearXNG's netns ref). Authentik ProxyProvider marreta registered (pk 13, external_host=https://marreta.franksdank.cloud/, internal_host=http://caddy:8017). Results:

  • Unauth edge → Anubis challenge page (not 502). TLS cert issued via DNS-01.
  • Post-auth sim (caddy :8017 with Host: marreta.franksdank.cloud) → 200.
  • marreta container health: healthy; / and /admin/login → 200.
  • Netns sharing proven: gluetun, marreta, marreta_browser all on the same net:[…] inode; marreta reads the CDP endpoint (GET 127.0.0.1:9222/json/version → Lightpanda/1.0).
  • SearXNG unaffected: :8081 → 200 after the tunnel recreate.

Gotcha hit during deploy: the first seed used a bare-base64 APP_KEY, which 500s every app route (see Secrets above). Fixed by re-keying with the base64: prefix before any encrypted data was persisted.

Citations