--- type: reference title: Marreta on Dankclaw description: "The content-cleaning proxy SearXNG borrows egress from: a quadlet pair in the VPN netns behind the guard chain." tags: - dankclaw - marreta - proxy - gluetun - authentik - quadlet timestamp: 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` — ) 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:** ## 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. ```mermaid 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 -rf`s 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: ```python # 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 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` (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 - Marreta upstream: (README, `compose.yml`, `bootstrap/app.php`, `app/Services/Fetch/BrowserFetchStrategy.php` `lang/` directory listing). - serversideup/php fpm-nginx port mechanism: `serversideup/docker-php` `src/variations/fpm-nginx/Dockerfile` (`NGINX_HTTP_PORT="8080"`) and `src/s6/etc/entrypoint.d/10-init-webserver-config.sh` (`envsubst` over `NGINX_*` at start; `conf.d/*.conf` removed at build). - Lightpanda browser image: (`nightly` tag). - Guard-chain ordering and provider-first rule: [[Anubis]], [[Authentik]], and the `anubis-guard-chain-flip` runbook. - Netns egress pattern: [[SearXNG]], [[Gluetun]], [[Container Network Namespaces]].