Import the MetaMesh app family from Worph/AppStore
23 apps (MetaCore, the library/network/client tiers and 13 MetaFeeder*) move out of the general-purpose Worph/AppStore into their own store so the MetaMesh family releases on its own cadence. - asset URLs repointed to cdn.jsdelivr.net/gh/Worph/MetaAppStore@main - store scaffolding added (category/recommend/featured lists, licence, contributing, gitattributes) - psd-source/meta-icons-v2 icon generator follows the apps MetaMCP stays in Worph/AppStore: it is third-party metatool-ai software, not part of MetaMesh.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# MetaGateway
|
||||
|
||||
Source-bridging tier for MetaMesh: ingests content from external sources and
|
||||
makes it discoverable across the federation, so **MetaWatch** (and other
|
||||
MetaShare peers) can find and stream it. Unifies the three dev gateway
|
||||
sub-stacks (`docker-compose.yml` + `.torznab.yml` + `.tribler.yml`) into one
|
||||
app sharing a single meta-core, ingester and enrichment plugin pair.
|
||||
|
||||
## What's in the stack (13 containers)
|
||||
|
||||
| Container | Role |
|
||||
|-----------|------|
|
||||
| `metagateway-app` | Gateway peer (feeders-only); writes resolved records to meta-core (dashboard behind OIDC) |
|
||||
| `metagateway-core` | Dedicated meta-core; auto-store target + WebDAV poster store (watcher ON) |
|
||||
| `metagateway-share` | meta-share **ingester**: announces resolved records on the swarm, seeds blobs |
|
||||
| `metagateway-prowlarr` | Bundled Prowlarr torznab aggregator (own public route + forms login) |
|
||||
| `metagateway-torznab-feeder` | torznab feeder → Prowlarr (+ librqbit BT fetch) |
|
||||
| `metagateway-tribler` | Headless Tribler core (IPv8 overlay) |
|
||||
| `metagateway-tribler-feeder` | tribler feeder → Tribler core |
|
||||
| `book-feeder` / `paper-feeder` / `common-feeder` | Gutenberg / arXiv+PubMed / Wikimedia Commons |
|
||||
| `metagateway-filename-parser` / `metagateway-tmdb` | Reused meta-sort enrichment plugins |
|
||||
| `metagateway` | nginx-hash-lock OIDC perimeter for the dashboard |
|
||||
|
||||
The gateway discovers its feeders from `gateway-config.json` (the `feeders`
|
||||
map), and `gateway-enabled.json` opts the `torznab` + `tribler` upstreams in.
|
||||
Both are written by `pre-install-cmd` into `/DATA/AppData/metagateway/state/` —
|
||||
edit them there and restart `metagateway-app` to change the feeder set.
|
||||
|
||||
## Self-contained meta-core
|
||||
|
||||
The gateway's meta-core is **private to the gateway** (it holds resolved feeder
|
||||
records, not your media library), so this app does **not** use the shared
|
||||
MetaCore app and coexists with MetaWatch + the standalone Meta* apps without
|
||||
container-name or data-path collisions.
|
||||
|
||||
```
|
||||
/DATA/AppData/metagateway/
|
||||
├── metacore/ # gateway meta-core (shared by core + ingester)
|
||||
├── files/ # WebDAV file store (tmdb posters/backdrops, seeded blobs)
|
||||
├── state/ # gateway-config.json + gateway-enabled.json + redb caches
|
||||
├── prowlarr/ # Prowlarr config + db
|
||||
├── tribler/ # Tribler IPv8 identity + metadata db (+ installed config)
|
||||
└── tmdb-cache/ # tmdb plugin response cache
|
||||
```
|
||||
|
||||
## First-run setup
|
||||
|
||||
1. **Prowlarr** — open `https://metagateway-prowlarr-<domain>/`, finish the
|
||||
wizard, add indexers, copy the **API Key**.
|
||||
2. Set `PROWLARR_API_KEY` and `TMDB_TOKEN` (TMDB v4 read token) on this app,
|
||||
then restart. Until set, the **torznab feeder reports as degraded** (the
|
||||
reason is visible at its `/health` and in the dashboard) but never crashes —
|
||||
the rest of the gateway stays healthy and the app boots normally. Optional:
|
||||
`GIPHY_API_KEY` (common-feeder), `SCIHUB_MIRRORS` (paper-feeder).
|
||||
3. **Tribler** needs no setup — the baked config installs on first boot.
|
||||
|
||||
## Federation
|
||||
|
||||
Two peers join the **public** libp2p swarm, on two different ports:
|
||||
|
||||
| Peer | Port | Role |
|
||||
|------|------|------|
|
||||
| `metagateway-app` | TCP **4002** | gateway **discovery** — publishes the kad provider record under the `metamesh-gateway` namespace; this is the peer a remote meta-search finds and dials for live fan-out |
|
||||
| `metagateway-share` | TCP **4005** | the ingester's records + bitswap byte-serving (cohort `metamesh-share-default`) |
|
||||
|
||||
Tribler's IPv8 overlay is UDP **8092**.
|
||||
|
||||
**Cross-node discovery needs both the port open inbound AND a public address
|
||||
set.** Without `META_GATEWAY_PUBLIC_ADDR` the gateway starts in local/mDNS-only
|
||||
mode: it pulls in no kad bootstraps, publishes no provider record, and binds an
|
||||
ephemeral port — so no off-host MetaWatch can ever find it, however open the
|
||||
firewall is. The compose sets it from `${APP_PUBLIC_IPV4}`; on a PCS whose
|
||||
public IP is not the v4 one, override it. Same-host MetaWatch discovery works
|
||||
over mDNS on `pcs` regardless.
|
||||
|
||||
## Images
|
||||
|
||||
| Image | Tag |
|
||||
|-------|-----|
|
||||
| `ghcr.io/worph/meta-gateway` | `1.0.25` |
|
||||
| `ghcr.io/worph/meta-share` | `1.0.26` |
|
||||
| `ghcr.io/worph/meta-core` | `1.0.12` |
|
||||
| `ghcr.io/yundera/appshield` | `2.0.9` |
|
||||
|
||||
No feeder images are listed here any more: every content source is a **separate
|
||||
store app**, one per provider (MetaFeeder · Gutenberg / arXiv / Europe PMC /
|
||||
Wikimedia Commons / Prowlarr / Tribler / …), each
|
||||
pinning its own images. The gateway discovers them by URL over `pcs` and
|
||||
soft-skips any that aren't installed.
|
||||
| `lscr.io/linuxserver/prowlarr` | `latest` |
|
||||
| `ghcr.io/yundera/nginx-hash-lock` | `1.0.7` |
|
||||
|
||||
**Tribler** uses the upstream `ghcr.io/tribler/tribler` image directly (there is
|
||||
no custom `meta-tribler` image). `pre-install-cmd` fetches the complete REST
|
||||
config (bound to `0.0.0.0:8085`, key `changeme`) to
|
||||
`/DATA/AppData/metagateway/tribler-config/configuration.json` and bind-mounts it
|
||||
at `/seed/configuration.json`; the entrypoint installs it into `/root/.Tribler`
|
||||
on first boot.
|
||||
|
||||
## Resilience notes
|
||||
|
||||
- **Feeders run as `user: "0:0"`.** Their image bakes a non-root uid (10001) as
|
||||
owner of the state dir, but the platform injects `user: 1000:1000` on services
|
||||
without an explicit `user:`; running as root avoids a state-dir permission
|
||||
crash.
|
||||
- **A misconfigured feeder never crash-loops.** The feeder SDK keeps a
|
||||
feeder whose config is bad/incomplete *running* and reports it as **degraded**
|
||||
via `/health` (with the reason) rather than exiting — so one bad key can't take
|
||||
the gateway down. The app gates torznab on `service_started`, not
|
||||
`service_healthy`, for the same reason.
|
||||
@@ -0,0 +1,580 @@
|
||||
name: metagateway
|
||||
|
||||
# =============================================================================
|
||||
# MetaGateway — bridges external sources into the MetaMesh network.
|
||||
#
|
||||
# A single gateway peer — the bridging tier ONLY. Every content source is now a
|
||||
# SEPARATE store app (install whichever you want); the gateway discovers each by
|
||||
# URL over `pcs`, stores its resolved records, and soft-skips any not installed:
|
||||
# MetaFeeder · Gutenberg Project Gutenberg (+ Open Library cards)
|
||||
# MetaFeeder · arXiv / Europe PMC academic papers (Sci-Hub: separate opt-in app)
|
||||
# MetaFeeder · Wikimedia Commons open-licensed media (Giphy: separate opt-in app)
|
||||
# MetaFeeder · Prowlarr Prowlarr indexers (video/torrent) — own enrichment
|
||||
# MetaFeeder · Tribler Tribler IPv8 overlay (video/torrent) — own enrichment
|
||||
# Plus a dedicated meta-core, and a co-located meta-share INGESTER that announces the
|
||||
# gateway's resolved records on the swarm.
|
||||
#
|
||||
# Everything is scoped under the `metagateway-*` prefix and the per-stack
|
||||
# `metagateway` network + /DATA/AppData/metagateway/ data root, so this stack
|
||||
# installs alongside MetaWatch and the standalone Meta* apps with ZERO
|
||||
# container-name / data-path collisions. Its meta-core is private to the
|
||||
# gateway (holds resolved feeder records, not your library) — hence its own
|
||||
# core rather than the shared MetaCore app.
|
||||
#
|
||||
# Federation: the gateway-app joins the PUBLIC IPFS DHT and is discovered for
|
||||
# live fan-out on host port 4002; the meta-share ingester also joins the public
|
||||
# swarm (records + bitswap byte-serving) on host port 4005 + mDNS on `pcs`, so
|
||||
# MetaWatch peers find both the gateway and its seeded bytes.
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ===========================================================================
|
||||
# metagateway-core — dedicated meta-core. Auto-store target for every resolved
|
||||
# feeder outcome; the gateway becomes a regular meta-core host of the CID.
|
||||
# File watcher ON so tmdb-plugin posters PUT over WebDAV get indexed + seeded.
|
||||
# ===========================================================================
|
||||
metagateway-core:
|
||||
# 1.0.1 carries the new meta-core API (records cids/ set, identity).
|
||||
image: ghcr.io/worph/meta-core:1.0.12
|
||||
container_name: metagateway-core
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 768m
|
||||
memswap_limit: 768m
|
||||
hostname: metagateway-core
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
user: "0:0"
|
||||
cpu_shares: 50
|
||||
expose:
|
||||
- 9000
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/metacore:/meta-core:rw
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/files:/files:rw
|
||||
environment:
|
||||
- BASE_URL=http://metagateway-core:9000
|
||||
- TZ=$TZ
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
# `pcs` so the external feeder apps can write records/posters to this core.
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
# ===========================================================================
|
||||
# metagateway-share — meta-share INGESTER role (META_CORE_URL set). Polls this
|
||||
# gateway's meta-core, announces resolved records on Kamilata + rebuilds its
|
||||
# Bloom filter, and seeds blobs over IPFS/bitswap so federated MetaWatch
|
||||
# queries reach the gateway's records. Public libp2p on host port 4005
|
||||
# (the gateway-app owns 4002 for discovery).
|
||||
# ===========================================================================
|
||||
metagateway-share:
|
||||
# 1.0.8 = rsa-enabled libp2p (joins the public IPFS DHT — see metagateway-app)
|
||||
# + P2P_ANNOUNCE support so remote peers can dial this bitswap seeder.
|
||||
image: ghcr.io/worph/meta-share:1.0.26
|
||||
container_name: metagateway-share
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 3g
|
||||
memswap_limit: 3g
|
||||
hostname: metagateway-share
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
cpu_shares: 30
|
||||
expose:
|
||||
- 3000
|
||||
# Own public libp2p port (4005). The gateway-app owns 4002 (discovery);
|
||||
# this ingester's bitswap tier serves the gateway's seeded bytes to remote
|
||||
# peers over the public DHT, so it needs its own dialable host port.
|
||||
ports:
|
||||
- "4005:4001"
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/metacore:/meta-core:rw
|
||||
# Operator settings (settings.json — NNTP creds + indexer API keys). This
|
||||
# is the CREDENTIALED peer of the pair: it materialises NZB postings for
|
||||
# credential-less clients. Losing settings.json on an update means the
|
||||
# gateway silently stops serving nzb-release bytes until re-entered.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/share-config:/config:rw
|
||||
# Blockstore + seed manifest + NZB cache (META_SHARE_DATA=/data/meta-share).
|
||||
# Without it the gateway's seeded corpus is wiped on every recreate.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/share-data:/data:rw
|
||||
environment:
|
||||
P2P_LISTEN: "/ip4/0.0.0.0/tcp/4001"
|
||||
# Advertise the routable address (published host port 4005) so kad
|
||||
# provider records + identify carry a dialable multiaddr for remote
|
||||
# bitswap fetches. Behind Docker NAT the swarm otherwise only sees
|
||||
# container-internal addresses.
|
||||
# ${APP_PUBLIC_IPV4}, NOT ${APP_PUBLIC_IP} — see the note on the
|
||||
# gateway-app below: APP_PUBLIC_IP is the v6 address on an
|
||||
# IPv6-primary PCS, and `/ip4/<v6>/tcp/...` fails to parse.
|
||||
PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4005"
|
||||
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001 (the nested
|
||||
# dnsaddr form resolves to wss-only in some container resolvers). rsa
|
||||
# feature required to complete the noise handshake with these RSA-keyed
|
||||
# (Qm...) bootstrap nodes.
|
||||
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
|
||||
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
|
||||
# It is here for resilience: the four IPFS-Foundation bootstrappers can
|
||||
# refuse an individual IP, and when they do a peer cannot join the DHT AT
|
||||
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
|
||||
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
|
||||
# reachable fifth was added. Costs nothing when the four work.
|
||||
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
|
||||
META_CORE_URL: "http://metagateway-core:9000"
|
||||
META_CORE_PATH: "/meta-core"
|
||||
BASE_URL: "http://metagateway-share:3000"
|
||||
# Renamed at the 1.0.26 bump (2026-08-26), as the old note here said to do:
|
||||
# META_SHARE_PEER_API_URL was the pre-1.0.8 spelling and now only logs a
|
||||
# deprecation warning. Do NOT set both.
|
||||
INTERNAL_BASE_URL: "http://metagateway-share:3000"
|
||||
TZ: "$TZ"
|
||||
depends_on:
|
||||
metagateway-core:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
# ===========================================================================
|
||||
# metagateway-app — the gateway peer (feeders-only). Discovers the feeder
|
||||
# sidecars from /data/meta-gateway/gateway-config.json (written by
|
||||
# pre-install-cmd), registers a RemoteFeederPlugin per upstream, and writes
|
||||
# resolved records to its meta-core. Dashboard fronted by the hash-lock below.
|
||||
# ===========================================================================
|
||||
metagateway-app:
|
||||
# 1.0.12 = rsa-enabled libp2p (required to join the public IPFS DHT: the
|
||||
# bootstrap.libp2p.io nodes are RSA-keyed, so without `rsa` the noise
|
||||
# handshake fails and the gateway never joins the DHT) + configurable
|
||||
# P2P listen/announce so it's discoverable + dialable off-host + the
|
||||
# 2026-07 discovery/filters fixes that make the meta-search fan-out
|
||||
# actually route `fileType:video` queries here.
|
||||
image: ghcr.io/worph/meta-gateway:1.0.25
|
||||
container_name: metagateway-app
|
||||
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
|
||||
# instead of swap. Mirrors the dev sub-stack limits.
|
||||
mem_limit: 1g
|
||||
memswap_limit: 1g
|
||||
hostname: metagateway-app
|
||||
restart: unless-stopped
|
||||
cpu_shares: 40
|
||||
expose:
|
||||
- 3000
|
||||
# Public libp2p listener (4002). This is the peer MetaWatch/meta-search
|
||||
# discovers via the kad `metamesh-gateway` provider namespace and dials
|
||||
# for live gateway fan-out (torznab/tribler). Must be host-published +
|
||||
# reachable inbound.
|
||||
ports:
|
||||
- "4002:4002"
|
||||
volumes:
|
||||
# gateway-config.json (feeders map) + gateway-enabled.json, written by
|
||||
# pre-install-cmd. Runtime redb caches also live here.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/state:/data/meta-gateway:rw
|
||||
environment:
|
||||
# NOTE: `meta_core_url` (and `meta_share_url`) live in gateway-config.json
|
||||
# since v1.0.12 — no longer read from env. The pre-install-cmd below seeds
|
||||
# them; the dashboard edits them at runtime. `META_GATEWAY_BASE_URL` stays
|
||||
# in env because it's an identify-layer setting (baseUrl= token in the
|
||||
# libp2p identify agent_version) — without it meta-search can't HTTP-fetch
|
||||
# /api/gateway/plugins on this gateway to populate its routing table.
|
||||
META_GATEWAY_PEER_ID: "metagateway"
|
||||
# MUST be resolvable BY A REMOTE PEER — it is advertised verbatim in the
|
||||
# identify `baseUrl=` token and used for nothing else in-process. A
|
||||
# docker-internal name here (the old `http://metagateway-app:3000`) makes
|
||||
# the gateway useless off-host even once kad discovery works: the remote
|
||||
# capability fetch fails, the snapshot stays empty, and
|
||||
# `upstreams_for_query_from_raw` then fans out to NOBODY. The
|
||||
# `/api/gateway/plugins` carve-out in the Caddy labels below is what makes
|
||||
# this URL answerable without a session.
|
||||
META_GATEWAY_BASE_URL: "https://metagateway-${APP_DOMAIN}"
|
||||
# Fixed libp2p port + advertise the routable address (published host
|
||||
# port 4002) so provider records + identify carry a dialable multiaddr.
|
||||
# Ceiling on established libp2p connections (new in meta-gateway 1.0.25).
|
||||
# Joining the public DHT is otherwise UNBOUNDED: at 1.0.24 this peer
|
||||
# climbed to ~600 connections and was OOM-killed by the memory limit
|
||||
# below roughly once a minute, and because each boot minted a fresh
|
||||
# peer-id, every provider record it published was stale within seconds —
|
||||
# remote peers only ever got "Unexpected peer ID". 128 is the default;
|
||||
# raise it only together with the memory limit.
|
||||
META_GATEWAY_MAX_CONNECTIONS: "128"
|
||||
META_GATEWAY_P2P_LISTEN: "/ip4/0.0.0.0/tcp/4002"
|
||||
# Setting a PUBLIC_ADDR is what flips the gateway out of local/mDNS-only
|
||||
# mode. Unset, meta-gateway pulls in NO kad bootstraps and sets
|
||||
# kad_provide_enabled=false, so it never publishes its `metamesh-gateway`
|
||||
# provider record and no off-host meta-search can discover it (it also
|
||||
# binds an ephemeral /tcp/0, leaving the `ports` mapping below with
|
||||
# nothing to forward to). See meta-gateway config.rs::resolve_network.
|
||||
# ${APP_PUBLIC_IPV4}, NOT ${APP_PUBLIC_IP}: on an IPv6-primary PCS,
|
||||
# APP_PUBLIC_IP holds the v6 address and `/ip4/<v6>/tcp/...` is not a
|
||||
# parseable multiaddr — the gateway refuses to start rather than
|
||||
# announce a lie. APP_PUBLIC_IPV4 is the v4 spelling, set on every box.
|
||||
META_GATEWAY_PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4002"
|
||||
# Public IPFS DHT bootstraps via explicit /dns4/.../tcp/4001 (the nested
|
||||
# dnsaddr default resolves to wss-only in some container resolvers).
|
||||
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
|
||||
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
|
||||
# It is here for resilience: the four IPFS-Foundation bootstrappers can
|
||||
# refuse an individual IP, and when they do a peer cannot join the DHT AT
|
||||
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
|
||||
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
|
||||
# reachable fifth was added. Costs nothing when the four work.
|
||||
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
|
||||
TZ: "$TZ"
|
||||
depends_on:
|
||||
metagateway-core:
|
||||
condition: service_healthy
|
||||
# ALL feeders (book/paper/common + torznab/tribler) are SEPARATE store apps
|
||||
# now — the gateway discovers them by URL from its feeders config and
|
||||
# soft-skips any not installed, so they're not startup deps here.
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
# `pcs` so the gateway can reach the external feeder apps for discovery/query.
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
# NOTE: ALL feeders are now SEPARATE store apps, one per provider — e.g.
|
||||
# MetaFeeder · Gutenberg / arXiv / Europe PMC / Wikimedia Commons and the torrent
|
||||
# ones (MetaFeeder · Prowlarr / Tribler). The gateway is purely the bridging tier:
|
||||
# it discovers every feeder by URL (feeders config / dashboard) over `pcs`,
|
||||
# stores their resolved records in its meta-core, and announces them on the
|
||||
# swarm. Install whichever feeders you want; the gateway soft-skips any not
|
||||
# reachable. No feeder sidecars are bundled here anymore.
|
||||
|
||||
# ===========================================================================
|
||||
# OIDC perimeter for the MetaGateway dashboard. container_name MUST equal the
|
||||
# URL prefix (`metagateway`) for the registrar's redirect-URI allowlist.
|
||||
# ===========================================================================
|
||||
metagateway:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metagateway
|
||||
hostname: metagateway
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose:
|
||||
- 80
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
BACKEND_HOST: "metagateway-app"
|
||||
BACKEND_PORT: "3000"
|
||||
LISTEN_PORT: "80"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
depends_on:
|
||||
- metagateway-app
|
||||
labels:
|
||||
# ---- metagateway-${APP_DOMAIN} (gateway TLS) ----
|
||||
caddy_0: metagateway-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.0_handle: "/health"
|
||||
caddy_0.0_handle.reverse_proxy: "metagateway-app:3000"
|
||||
# Capability contract for REMOTE peers that have no session.
|
||||
# Exact-match path (no trailing `*`), so mutating routes like
|
||||
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
|
||||
caddy_0.1_handle: "/api/gateway/plugins"
|
||||
caddy_0.1_handle.reverse_proxy: "metagateway-app:3000"
|
||||
caddy_0.2_handle: ""
|
||||
caddy_0.2_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
|
||||
# ---- metagateway-IP.nip.io (gateway TLS) ----
|
||||
caddy_1: metagateway-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.0_handle: "/health"
|
||||
caddy_1.0_handle.reverse_proxy: "metagateway-app:3000"
|
||||
# Capability contract for REMOTE peers that have no session.
|
||||
# Exact-match path (no trailing `*`), so mutating routes like
|
||||
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
|
||||
caddy_1.1_handle: "/api/gateway/plugins"
|
||||
caddy_1.1_handle.reverse_proxy: "metagateway-app:3000"
|
||||
caddy_1.2_handle: ""
|
||||
caddy_1.2_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
|
||||
# ---- metagateway-IP.sslip.io (Let's Encrypt) ----
|
||||
caddy_2: metagateway-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.0_handle: "/health"
|
||||
caddy_2.0_handle.reverse_proxy: "metagateway-app:3000"
|
||||
# Capability contract for REMOTE peers that have no session.
|
||||
# Exact-match path (no trailing `*`), so mutating routes like
|
||||
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
|
||||
caddy_2.1_handle: "/api/gateway/plugins"
|
||||
caddy_2.1_handle.reverse_proxy: "metagateway-app:3000"
|
||||
caddy_2.2_handle: ""
|
||||
caddy_2.2_handle.reverse_proxy: "{{upstreams 80}}"
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
# ===========================================================================
|
||||
# AppShield gates — expose the bundled service UIs behind Yundera OIDC SSO so
|
||||
# they're reachable + configurable from the dashboard's service links.
|
||||
# AppShield's container_name MUST equal its subdomain prefix (the auth-registrar
|
||||
# derives the OIDC client_id from it), so these use collision-free prefixes —
|
||||
# `metagateway-core`/`-share` are already the backend names. These backends
|
||||
# have no native auth, so they're reachable ONLY through these gates.
|
||||
# metagatewaycore → meta-core dashboard (nginx :80, NOT the :9000 API)
|
||||
# metagatewayshare → meta-share dashboard (:3000)
|
||||
# Prowlarr + Tribler are SEPARATE store apps (their own UIs + auth); the
|
||||
# feeders reach them over `pcs`, so there's no gate for them here.
|
||||
# ===========================================================================
|
||||
metagatewaycore:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metagatewaycore
|
||||
hostname: metagatewaycore
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose: ["80"]
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
BACKEND_HOST: "metagateway-core"
|
||||
BACKEND_PORT: "80"
|
||||
LISTEN_PORT: "80"
|
||||
labels:
|
||||
caddy_0: metagatewaycore-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_1: metagatewaycore-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_2: metagatewaycore-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.reverse_proxy: "{{upstreams 80}}"
|
||||
depends_on:
|
||||
- metagateway-core
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
metagatewayshare:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
cpu_shares: 80
|
||||
container_name: metagatewayshare
|
||||
hostname: metagatewayshare
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
expose: ["80"]
|
||||
environment:
|
||||
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
||||
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
|
||||
BACKEND_HOST: "metagateway-share"
|
||||
BACKEND_PORT: "3000"
|
||||
LISTEN_PORT: "80"
|
||||
labels:
|
||||
caddy_0: metagatewayshare-${APP_DOMAIN}
|
||||
caddy_0.import: gateway_tls
|
||||
caddy_0.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_1: metagatewayshare-${APP_PUBLIC_IP_DASH}.nip.io
|
||||
caddy_1.import: gateway_tls
|
||||
caddy_1.reverse_proxy: "{{upstreams 80}}"
|
||||
caddy_2: metagatewayshare-${APP_PUBLIC_IP_DASH}.sslip.io
|
||||
caddy_2.reverse_proxy: "{{upstreams 80}}"
|
||||
depends_on:
|
||||
- metagateway-share
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
networks:
|
||||
metagateway:
|
||||
name: metagateway
|
||||
driver: bridge
|
||||
pcs:
|
||||
name: ${APP_NET:-pcs}
|
||||
external: true
|
||||
|
||||
x-casaos:
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
author: Worph Team
|
||||
category: Media
|
||||
# `needs-public-ip` is the reserved functional tag: this app publishes a raw
|
||||
# libp2p TCP port that remote peers must dial inbound. Behind CGNAT or the
|
||||
# mesh-router tunnel it still installs and starts, but peers cannot reach it.
|
||||
tags: ["media", "needs-public-ip"]
|
||||
developer: Aptero Team
|
||||
|
||||
description:
|
||||
en_us: |
|
||||
**The source-bridging tier for the MetaMesh network.**
|
||||
|
||||
MetaGateway ingests content from external sources and makes it
|
||||
discoverable across the MetaMesh federation — so MetaWatch (and other
|
||||
MetaShare peers) can find and stream it. It's the **bridging tier only**:
|
||||
a dedicated meta-core that stores resolved records, plus a co-located
|
||||
meta-share ingester that announces them on the swarm. Every content source
|
||||
is a **separate MetaFeeder app**, one per provider (Prowlarr, Tribler, Usenet,
|
||||
Gutenberg, arXiv, Europe PMC, Wikimedia Commons, …)
|
||||
— install whichever you want; the gateway discovers each over the shared
|
||||
network and soft-skips any not installed.
|
||||
|
||||
**Sources (each a separate store app):**
|
||||
- **Prowlarr** - Torznab/Newznab indexers via the separate Prowlarr app (video/torrent/usenet)
|
||||
- **Tribler** - Decentralized Tribler discovery (video/torrent)
|
||||
- **Project Gutenberg** - Public-domain books (+ Open Library work cards)
|
||||
- **arXiv / Europe PMC** - Academic papers (Sci-Hub: separate opt-in app)
|
||||
- **Wikimedia Commons** - Open-licensed images (Giphy: separate opt-in app)
|
||||
|
||||
**How it fits:** MetaGateway is the *producer*; **MetaWatch** is the
|
||||
*consumer*. Install MetaGateway to populate the network, then browse it in
|
||||
MetaWatch. Self-contained — its meta-core is private to the gateway, so it
|
||||
coexists with MetaWatch and the standalone Meta* apps without conflicts.
|
||||
|
||||
**Enrichment:** Each resolved release is enriched with filename parsing and
|
||||
TMDB metadata (canonical content-addressed posters + plot) so it surfaces
|
||||
cleanly in MetaWatch's poster-gated wall.
|
||||
|
||||
fr_fr: |
|
||||
**La couche de passerelle de sources pour le réseau MetaMesh.**
|
||||
|
||||
MetaGateway ingère du contenu depuis des sources externes et le rend
|
||||
découvrable à travers la fédération MetaMesh — pour que MetaWatch (et les
|
||||
autres pairs MetaShare) puissent le trouver et le lire. Elle exécute tout
|
||||
le lot de feeders en sidecars, plus un Prowlarr intégré, un cœur Tribler
|
||||
headless, les plugins d'enrichissement meta-sort, un meta-core dédié et un
|
||||
ingesteur meta-share co-localisé qui annonce les enregistrements résolus.
|
||||
|
||||
**Sources (chacune une app du store) :**
|
||||
- **Prowlarr** - Indexeurs Torznab/Newznab via l'app Prowlarr (vidéo/torrent/usenet)
|
||||
- **Tribler** - Découverte décentralisée Tribler (vidéo/torrent)
|
||||
- **Project Gutenberg** - Livres du domaine public (+ fiches Open Library)
|
||||
- **arXiv / Europe PMC** - Articles académiques (Sci-Hub : app optionnelle séparée)
|
||||
- **Wikimedia Commons** - Images sous licence libre (Giphy : app optionnelle séparée)
|
||||
|
||||
**Place dans l'écosystème :** MetaGateway est le *producteur* ; **MetaWatch**
|
||||
est le *consommateur*. Installez MetaGateway pour alimenter le réseau, puis
|
||||
parcourez-le dans MetaWatch. Autonome — son meta-core est privé, donc il
|
||||
cohabite avec MetaWatch et les apps Meta* sans conflit.
|
||||
|
||||
tagline:
|
||||
en_us: Bridge torrent/usenet, books, papers and images into the MetaMesh network
|
||||
fr_fr: Connectez torrent/usenet, livres, articles et images au réseau MetaMesh
|
||||
|
||||
title:
|
||||
en_us: MetaGateway
|
||||
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
|
||||
screenshot_link:
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-1.png
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-2.png
|
||||
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-3.png
|
||||
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/thumbnail.png
|
||||
|
||||
index: /
|
||||
store_app_id: metagateway
|
||||
main: metagateway
|
||||
is_uncontrolled: false
|
||||
|
||||
tips:
|
||||
before_install:
|
||||
en_us: |
|
||||
**Self-contained — no MetaCore required.** MetaGateway ships its own
|
||||
meta-core, ingester and feeders. Pair it with **MetaWatch** to browse
|
||||
what it surfaces.
|
||||
|
||||
**Authentication:** The gateway dashboard is protected by Yundera's
|
||||
Authelia via OIDC.
|
||||
|
||||
**The gateway works out of the box.** Books, papers and common-media
|
||||
feeders need no setup. Torrent/video needs the separate **Prowlarr** and
|
||||
**Tribler** apps — until they're installed + configured, those feeders
|
||||
simply show as **"degraded"** in the dashboard (they don't crash the app).
|
||||
|
||||
**Torznab / Prowlarr setup (required for torrent video):**
|
||||
1. Install the **Prowlarr** app from the store (it runs on the shared
|
||||
network as `prowlarr`).
|
||||
2. Open Prowlarr, complete its first-run wizard, then Indexers → Add
|
||||
Indexer → add one or more public indexers (confirm they test green).
|
||||
3. Settings → General → copy the **API Key**.
|
||||
4. In the gateway dashboard, open the **torznab** plugin → configure, paste
|
||||
the key into the indexer's API-key field, and Save. The torznab feeder
|
||||
auto-reloads and turns healthy.
|
||||
|
||||
**Tribler (optional, decentralized torrents):** install the **Tribler**
|
||||
app from the store; the gateway's tribler feeder connects to it
|
||||
automatically over the shared network — no key needed.
|
||||
|
||||
**TMDB enrichment:** set the TMDB v4 token in the torznab / tribler plugin
|
||||
config (dashboard) for canonical posters + overviews.
|
||||
|
||||
**Federation ports (optional).** The gateway-app host-publishes its
|
||||
libp2p listener on TCP **4002** (discovery / live fan-out) and the
|
||||
meta-share ingester on TCP **4005** (records + bitswap byte-serving) —
|
||||
open both inbound for best cross-node reach; same-host discovery (a
|
||||
MetaWatch on the same box) works over mDNS without them.
|
||||
|
||||
pre-install-cmd: |
|
||||
# Feeders map: all feeders are now SEPARATE store apps, listed by their pcs
|
||||
# URLs (MetaFeeder · Gutenberg / arXiv / Europe PMC / Wikimedia Commons /
|
||||
# Prowlarr / Tribler — the opt-in Sci-Hub and Giphy apps are not seeded). The
|
||||
# gateway soft-skips any feeder that isn't installed/reachable, and you can
|
||||
# add/remove feeders at runtime from the dashboard. Each key/URL MUST match
|
||||
# the corresponding app's feeder container hostname.
|
||||
# Guarded: this hook reruns on every reinstall and every version upgrade, and
|
||||
# the feeder map is editable from the dashboard — an unguarded `cat >` would
|
||||
# silently discard the operator's feeders each time.
|
||||
if [ ! -f /DATA/AppData/metagateway/state/gateway-config.json ]; then
|
||||
cat > /DATA/AppData/metagateway/state/gateway-config.json <<'JSON'
|
||||
{
|
||||
"meta_core_url": "http://metagateway-core:9000",
|
||||
"meta_share_url": "http://metagateway-share:3000",
|
||||
"feeders": {
|
||||
"gutenberg-feeder": { "url": "http://metafeedergutenberg-feeder:8080" },
|
||||
"arxiv-feeder": { "url": "http://metafeederarxiv-feeder:8080" },
|
||||
"europepmc-feeder": { "url": "http://metafeedereuropepmc-feeder:8080" },
|
||||
"wikicommons-feeder": { "url": "http://metafeederwikicommons-feeder:8080" },
|
||||
"prowlarr-feeder": { "url": "http://metafeederprowlarr-feeder:8080" },
|
||||
"tribler-feeder": { "url": "http://metafeedertribler-feeder:8080" }
|
||||
}
|
||||
}
|
||||
JSON
|
||||
fi
|
||||
if [ ! -f /DATA/AppData/metagateway/state/gateway-enabled.json ]; then
|
||||
cat > /DATA/AppData/metagateway/state/gateway-enabled.json <<'JSON'
|
||||
{
|
||||
"disabled": [],
|
||||
"enabled_opt_in": ["torznab", "tribler"]
|
||||
}
|
||||
JSON
|
||||
fi
|
||||
|
||||
# Maison (Yundera dashboard) configuration. Read in preference to x-casaos.
|
||||
# schema_version 2 is required for `folders`: an older Maison refuses the app
|
||||
# outright rather than starting it without its directories.
|
||||
x-compose-app:
|
||||
schema_version: 2
|
||||
# Created and chowned by Maison before any image is pulled, before any hook
|
||||
# runs and before the containers start — on the first boot and every one after.
|
||||
# Compose would otherwise create a missing bind source as a root-owned dir.
|
||||
folders:
|
||||
- path: /DATA/AppData/metagateway/metacore
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metagateway/files
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metagateway/state
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metagateway/share-config
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metagateway/share-data
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# Same string as the caddy_0 label, so the tile link and the route agree.
|
||||
webui-host: metagateway-${domain}
|
||||
webui-path: "/"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1,64 @@
|
||||
# MetaGateway — Rationale
|
||||
|
||||
## What deviation / exception is being requested
|
||||
|
||||
1. `metagateway-core` and `metagateway-share` run `user: 0:0`, and `metagateway-core` runs
|
||||
`privileged: true`.
|
||||
2. The stack publishes two host ports: `4002/tcp` (gateway discovery) and `4005/tcp`
|
||||
(meta-share ingester bitswap).
|
||||
3. The app exposes **three** public hostnames — `metagateway-`, `metagatewaycore-` and
|
||||
`metagatewayshare-` — each with its own AppShield sidecar, so two sidecar
|
||||
`container_name`s differ from the compose top-level `name:`.
|
||||
|
||||
## Why it is necessary
|
||||
|
||||
1. `metagateway-core` is a bundled MetaCore: it runs rclone remote mounts, which need a
|
||||
mount namespace (`SYS_ADMIN` + `/dev/fuse` + an unconfined AppArmor profile), plus nginx
|
||||
and Redis. `metagateway-share` writes its service registration into the root-owned
|
||||
`/meta-core/services/`.
|
||||
2. libp2p is **not HTTP** — Caddy cannot proxy it, and remote peers resolve this node's
|
||||
public multiaddr from the kad-DHT and dial it directly. Without inbound reachability the
|
||||
node makes outbound connections only and never serves bytes, which is the app's function.
|
||||
The app therefore carries the reserved `needs-public-ip` tag.
|
||||
3. Each hostname is a distinct UI (gateway dashboard, core dashboard, share dashboard) and
|
||||
`auth-registrar` derives an app's OIDC `client_id` from the sidecar's container name via
|
||||
PTR lookup. Three routes need three stable, distinct names; only one of them can equal
|
||||
the top-level `name:`. `x-casaos.main` points at `metagateway`, the sidecar carrying the
|
||||
primary route.
|
||||
|
||||
## Security mitigations in place
|
||||
|
||||
- Every publicly-routed hostname terminates at its own AppShield sidecar
|
||||
(`ghcr.io/yundera/appshield:2.0.9`) behind the PCS's Authelia SSO. Each sidecar sets
|
||||
`hostname:` equal to its `container_name` so `auth-registrar` can attest it via PTR and
|
||||
accept its redirect URIs; the names are app-prefixed, so they cannot collide with another
|
||||
app's services in the shared `pcs` DNS namespace.
|
||||
- The backends carry no Caddy labels and publish **no HTTP port** to the host. The only
|
||||
published ports are raw libp2p TCP.
|
||||
- libp2p transport is authenticated and encrypted (Noise) and peer identity is a public
|
||||
key, so an unauthenticated dial cannot read anything the node has not chosen to publish
|
||||
to the swarm.
|
||||
- No user directories are mounted — no `/DATA/Documents`, `/DATA/Downloads`, `/DATA/Media`
|
||||
or `/DATA/Gallery`.
|
||||
- `cpu_shares` on every service; memory-heavy services carry a hard `mem_limit` with
|
||||
`memswap_limit` equal to it. All image tags pinned.
|
||||
|
||||
## Alternatives considered and rejected
|
||||
|
||||
- **Proxy libp2p through Caddy.** Not possible; the DHT advertises a raw TCP multiaddr.
|
||||
- **One sidecar with path-based routing to all three UIs.** The three backends generate
|
||||
absolute URLs and set cookies on their own hostnames; path-mounting them breaks both.
|
||||
- **Drop `privileged` from `metagateway-core` for a `cap_add` list.** The reduced form is
|
||||
most of what `privileged` grants and still failed to bring up the supervised daemons —
|
||||
see `Apps/MetaCore/rationale.md`, which is the same container.
|
||||
- **Split into separate store apps.** That is exactly what MetaCore / MetaShare /
|
||||
MetaGatewayShared are. This app is the self-contained variant, for a box that wants the
|
||||
gateway without the rest of the ecosystem installed.
|
||||
|
||||
## Data protection
|
||||
|
||||
All state lives under `/DATA/AppData/metagateway/`, declared in `x-compose-app.folders` and
|
||||
archived by Maison on uninstall. The two operator-editable config seeds
|
||||
(`gateway-config.json`, `gateway-enabled.json`) are written only when absent, so a reinstall
|
||||
or version upgrade preserves the feeder map the user configured from the dashboard. Only
|
||||
content published to the swarm leaves the box.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Reference in New Issue
Block a user