marreta: content-cleaning proxy on Dankclaw, gluetun netns, guard-chained
Deploy manualdousuario/marreta (Laravel / serversideup php:8.4-fpm-nginx) as a rootless Quadlet pair (marreta + marreta-browser/Lightpanda), both joining gluetun's netns so all fetch egress exits the same ProtonVPN tunnel as SearXNG. - marreta relocated to NGINX_HTTP_PORT=8085 (netns already has searxng on 8080); gluetun publishes 8082:8085; Caddy dials 192.168.0.143:8082. - Guard chain: marreta.franksdank.cloud -> Anubis :8923 -> Authentik -> post-auth :8017 @marreta -> 8082. Access = any authenticated user. - Secrets seeded to /home/dank/Containers/marreta/env (0600) from MARRETA_APP_KEY / MARRETA_ADMIN_EMAIL / MARRETA_ADMIN_PASSWORD. - deploy-plan.py: marreta joins the gluetun netns family. - New seam tests/marreta_quadlet.bats (17 contracts); scoping test updated for the widened netns family. Doc: Dankclaw/Marreta.md + index + log.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
---
|
||||
type: reference
|
||||
title: Marreta on Dankclaw
|
||||
description: The content-cleaning proxy (manualdousuario/marreta, Laravel/serversideup fpm-nginx) deployed as a rootless Quadlet pair that shares SearXNG's Gluetun netns for egress and sits behind the Anubis + Authentik guard chain at marreta.franksdank.cloud.
|
||||
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, 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.
|
||||
- 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).
|
||||
|
||||
## 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` | `gluetun.container` | `8085` (nginx) |
|
||||
| `marreta-browser.service` | `dankclaw/marreta/quadlet/marreta-browser.container` | `docker.io/lightpanda/browser:nightly` | `gluetun.container` | `9222` (CDP) |
|
||||
|
||||
Both units are boot-persistent (`WantedBy=default.target`) and self-healing
|
||||
(`Restart=always`), per the repo's Boot Persistence rule.
|
||||
|
||||
## 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, exactly as it publishes `8081:8080`
|
||||
for SearXNG. Caddy dials `192.168.0.143:8082`.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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}`.
|
||||
2. **Auth decision** — Anubis dials Caddy's `http://:8016`, which proxies
|
||||
whole-request to `authentik-server:9000`.
|
||||
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`.
|
||||
|
||||
**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
|
||||
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')`
|
||||
and forces `https` when `APP_URL` starts with `https://`. With
|
||||
`APP_URL=https://marreta.franksdank.cloud` 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`):
|
||||
|
||||
| Repo `.env` key | Mapped to (app) | Notes |
|
||||
|-----------------|-----------------|-------|
|
||||
| `MARRETA_APP_KEY` | `APP_KEY` | Laravel encryption key (`openssl rand -base64 32`). 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.
|
||||
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).
|
||||
|
||||
## Deploy
|
||||
|
||||
```
|
||||
./scripts/deploy.sh dankclaw --service marreta
|
||||
```
|
||||
|
||||
`deploy_marreta` 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
|
||||
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.
|
||||
- **Post-auth routing:** `podman exec caddy sh -c "grep -n -A3 '@marreta host' /etc/caddy/Caddyfile"`
|
||||
after the deploy's reload line.
|
||||
- **Egress:** from inside the netns, `podman exec marreta curl -s https://ifconfig.co`
|
||||
must return the VPN exit IP, not the home IP.
|
||||
- **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).
|
||||
|
||||
## Citations
|
||||
|
||||
- Marreta upstream: <https://github.com/manualdousuario/marreta> (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: <https://hub.docker.com/r/lightpanda/browser>
|
||||
(`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]].
|
||||
Reference in New Issue
Block a user