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,341 @@
|
||||
name: metagateway
|
||||
|
||||
# =============================================================================
|
||||
# MetaGateway (shared backend) — source-bridging tier for the MetaMesh network.
|
||||
#
|
||||
# This is the SHARED-BACKEND variant of MetaGateway. It ships only the gateway
|
||||
# dispatcher plus the card feeder, and REUSES the standalone MetaCore and
|
||||
# MetaShare apps already installed on the box:
|
||||
#
|
||||
# metagateway-app → the dispatcher / gateway UI
|
||||
# metagateway-card → the card feeder (curated-row cards for MetaWatch's wall)
|
||||
# metagateway → AppShield OIDC gate in front of the dispatcher
|
||||
#
|
||||
# Contrast with the `MetaGateway` app in this store, which is SELF-CONTAINED
|
||||
# (its own metagateway-core + metagateway-share + bundled Prowlarr/Tribler).
|
||||
# Pick exactly one: both use compose project `metagateway` and the same
|
||||
# /DATA/AppData/metagateway paths, so they cannot be installed side by side.
|
||||
#
|
||||
# Deployed and validated on watch.nsl.sh (2026-08-04).
|
||||
#
|
||||
# REQUIRES, on the same box and on the `pcs` network:
|
||||
# - MetaCore → provides metacore-app (record store the dispatcher writes to)
|
||||
# - MetaShare → provides metashare-app (seeds the CIDs the gateway resolves)
|
||||
# Indexer sources (torznab/Prowlarr and friends) are configured at runtime in
|
||||
# gateway-config.json under /DATA/AppData/metagateway/state — not here. Install
|
||||
# the separate **Prowlarr** and **MetaFeederProwlarr** apps to feed it.
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# metagateway-app — the gateway dispatcher. Resolves external-source queries
|
||||
# into MetaMesh records and writes them to the standalone metacore-app, whose
|
||||
# co-located metashare-app then seeds the resolved CIDs.
|
||||
# ---------------------------------------------------------------------------
|
||||
metagateway-app:
|
||||
image: ghcr.io/worph/meta-gateway:1.0.25
|
||||
container_name: metagateway-app
|
||||
hostname: metagateway-app
|
||||
restart: unless-stopped
|
||||
user: "1000:1000"
|
||||
cpu_shares: 40
|
||||
# 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
|
||||
expose:
|
||||
- 3000
|
||||
# Public libp2p listener. This is the peer a remote MetaWatch's meta-search
|
||||
# discovers via the kad `metamesh-gateway` provider namespace and dials for
|
||||
# live gateway fan-out. Must be host-published AND reachable inbound.
|
||||
ports:
|
||||
- "4002:4002"
|
||||
volumes:
|
||||
# gateway-config.json (source definitions, feeder URLs) + dispatcher state.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/state:/data/meta-gateway:rw
|
||||
environment:
|
||||
# Advertised verbatim in this peer's libp2p identify `agent_version`
|
||||
# (`baseUrl=...`) and used for NOTHING else in-process — see
|
||||
# meta-gateway `main.rs`: "prod compose would set it to the public
|
||||
# hostname behind whatever perimeter is in front."
|
||||
#
|
||||
# It MUST be resolvable BY A REMOTE PEER. It used to be
|
||||
# `http://metagateway-app:3000`, a docker-internal name, which made the
|
||||
# gateway useless off-host even once discovery worked: a remote
|
||||
# meta-search reads the served file_types/content_kinds by plain HTTP
|
||||
# `GET {baseUrl}/api/gateway/plugins` (the contract in the mirrored
|
||||
# `gateway_discovery.rs`), and an empty capability snapshot means
|
||||
# `upstreams_for_query_from_raw` fans out to NOBODY — every remote
|
||||
# typed query silently returns zero gateway hits. The `/api/gateway/plugins`
|
||||
# carve-out in the Caddy labels below is what makes this URL answerable.
|
||||
META_GATEWAY_BASE_URL: "https://metagateway-${APP_DOMAIN}"
|
||||
META_GATEWAY_PEER_ID: "metagateway"
|
||||
# --- cross-node discoverability (added 2026-08-26) ---------------------
|
||||
# WITHOUT META_GATEWAY_PUBLIC_ADDR meta-gateway starts in local/mDNS-only
|
||||
# mode: it loads NO kad bootstraps, sets kad_provide_enabled=false, and so
|
||||
# never publishes its `metamesh-gateway` provider record — no off-host
|
||||
# MetaWatch can EVER discover it, however open the firewall is. It also
|
||||
# binds an ephemeral /tcp/0, so the `ports` mapping above would have
|
||||
# nothing to forward to. It says so in its own first three log lines:
|
||||
# network_mode="local" kad_bootstrap_count=0 kad_provide_enabled=false
|
||||
# Verify after install: /api/peers → network_mode "public" + a non-empty
|
||||
# external_addrs. (This is the gap that made watch.nsl.sh invisible to the
|
||||
# wisera MetaWatch until 2026-08-26 — the self-contained MetaGateway
|
||||
# listing always had it; this shared-backend variant did not.)
|
||||
#
|
||||
# Use the IPV4 spelling, NOT the bare APP_PUBLIC_IP: on an IPv6-primary
|
||||
# PCS that variable holds the v6 address, and `/ip4/<v6>/tcp/...` is not a
|
||||
# parseable multiaddr — the gateway REFUSES TO START rather than announce
|
||||
# a lie. watch.nsl.sh is exactly such a box.
|
||||
# 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"
|
||||
META_GATEWAY_PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4002"
|
||||
# Public IPFS DHT bootstraps by 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-card:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
labels:
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
|
||||
networks:
|
||||
- metagateway
|
||||
- pcs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# metagateway-card — the CARD feeder. Answers the curated-row card queries
|
||||
# that populate MetaWatch's home wall (one query per row, no per-title
|
||||
# anchoring). Optional: the dispatcher starts without it, but the wall on
|
||||
# every connected MetaWatch stays empty.
|
||||
#
|
||||
# Runs as 10001:999 — the uid/gid baked into the meta-feeder-tmdb image.
|
||||
# ---------------------------------------------------------------------------
|
||||
metagateway-card:
|
||||
image: ghcr.io/worph/meta-feeder-tmdb:1.0.2
|
||||
container_name: metagateway-card
|
||||
hostname: metagateway-card
|
||||
restart: unless-stopped
|
||||
user: "10001:999"
|
||||
cpu_shares: 30
|
||||
mem_limit: 256m
|
||||
memswap_limit: 256m
|
||||
expose:
|
||||
- 8080
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/metagateway/card:/data/meta-feeder:rw
|
||||
environment:
|
||||
RUST_LOG: "info"
|
||||
TZ: "$TZ"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
labels:
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
|
||||
networks:
|
||||
- metagateway
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AppShield gate — the public entrypoint to the dispatcher UI, behind Yundera
|
||||
# OIDC SSO. container_name MUST equal the subdomain prefix (`metagateway`):
|
||||
# auth-registrar derives the OIDC client_id from it.
|
||||
#
|
||||
# /health is handled ahead of the gate so probes don't need a session.
|
||||
# `pcs` is REQUIRED — the gate resolves auth-registrar and casaos-oidc-bridge
|
||||
# by name on that network.
|
||||
# ---------------------------------------------------------------------------
|
||||
metagateway:
|
||||
image: ghcr.io/yundera/appshield:2.0.9
|
||||
container_name: metagateway
|
||||
hostname: metagateway
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
cpu_shares: 90
|
||||
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"
|
||||
# Gateway capability contract, read by REMOTE meta-search/meta-share
|
||||
# peers that have no session and never will. Exact-match path (no
|
||||
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
|
||||
# every `/api/metadata/*` write stay behind AppShield.
|
||||
# Response is capability-only: plugin ids + file_types/content_kinds
|
||||
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
|
||||
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"
|
||||
# Gateway capability contract, read by REMOTE meta-search/meta-share
|
||||
# peers that have no session and never will. Exact-match path (no
|
||||
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
|
||||
# every `/api/metadata/*` write stay behind AppShield.
|
||||
# Response is capability-only: plugin ids + file_types/content_kinds
|
||||
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
|
||||
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"
|
||||
# Gateway capability contract, read by REMOTE meta-search/meta-share
|
||||
# peers that have no session and never will. Exact-match path (no
|
||||
# trailing `*`), so the mutating `PUT /api/gateway/plugins/:id` and
|
||||
# every `/api/metadata/*` write stay behind AppShield.
|
||||
# Response is capability-only: plugin ids + file_types/content_kinds
|
||||
# + nzbFetch + nzbIndexers (indexer HOSTNAMES, no API keys).
|
||||
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}}"
|
||||
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
|
||||
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
|
||||
developer: Aptero Team
|
||||
hostname: metagateway-${APP_DOMAIN}
|
||||
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
|
||||
index: /
|
||||
is_uncontrolled: false
|
||||
main: metagateway
|
||||
port_map: "443"
|
||||
scheme: https
|
||||
store_app_id: metagatewayshared
|
||||
title:
|
||||
en_us: MetaGateway (shared backend)
|
||||
tagline:
|
||||
en_us: MetaGateway reusing the standalone MetaCore + MetaShare apps
|
||||
fr_fr: MetaGateway réutilisant les apps MetaCore et MetaShare autonomes
|
||||
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.
|
||||
|
||||
This build is the **shared-backend** variant: it ships the dispatcher and
|
||||
the card feeder only, and **requires** the standalone MetaCore and
|
||||
MetaShare apps on the same box, which it reuses for the record store and
|
||||
for seeding resolved CIDs. One core, one share — no duplicated services.
|
||||
If you want a stack that bundles its own core, share, Prowlarr and
|
||||
Tribler instead, install **MetaGateway**.
|
||||
|
||||
**How it fits:** MetaGateway is the *producer*; **MetaWatch** is the
|
||||
*consumer*. Install MetaGateway to populate the network, then browse it in
|
||||
MetaWatch.
|
||||
|
||||
**Sources** are configured at runtime in `gateway-config.json`; install
|
||||
**Prowlarr** and **MetaFeederProwlarr** (and optionally
|
||||
**MetaFeederUsenet**) to supply them.
|
||||
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.
|
||||
|
||||
Cette version est la variante **backend partagé** : elle n'embarque que le
|
||||
dispatcher et le feeder de cartes, et **nécessite** les apps MetaCore et
|
||||
MetaShare autonomes sur la même machine. Un seul cœur, un seul share —
|
||||
aucun doublon.
|
||||
|
||||
**Place dans l'écosystème :** MetaGateway est le *producteur* ; MetaWatch
|
||||
est le *consommateur*.
|
||||
tips:
|
||||
before_install:
|
||||
en_us: |
|
||||
**Requires MetaCore + MetaShare.** Install both standalone apps first —
|
||||
this build reuses them instead of bundling its own copies.
|
||||
|
||||
**Conflicts with `MetaGateway`.** Both use the compose project
|
||||
`metagateway` and the same data paths. Install one or the other.
|
||||
|
||||
**Sources are runtime config.** After install, define your sources in
|
||||
`/DATA/AppData/metagateway/state/gateway-config.json` (or through the
|
||||
dispatcher UI). Install **Prowlarr** + **MetaFeederProwlarr** to give it
|
||||
something to query.
|
||||
|
||||
**The card feeder matters.** `metagateway-card` answers the curated-row
|
||||
card queries that fill MetaWatch's home wall. Without it the dispatcher
|
||||
still works for search, but every connected MetaWatch shows an empty
|
||||
wall.
|
||||
|
||||
# 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/state
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/metagateway/card
|
||||
user: 10001
|
||||
group: 999
|
||||
# Same string as the caddy_0 label, so the tile link and the route agree.
|
||||
webui-host: metagateway-${domain}
|
||||
webui-path: "/"
|
||||
@@ -0,0 +1,52 @@
|
||||
# MetaGateway (shared backend) — Rationale
|
||||
|
||||
## What deviation / exception is being requested
|
||||
|
||||
1. `metagateway-card` runs as `10001:999` rather than `$PUID:$PGID`.
|
||||
2. The stack reuses the **standalone MetaCore and MetaShare apps'** services over the shared
|
||||
`pcs` network instead of bundling its own.
|
||||
3. The compose top-level `name:` is `metagateway`, the same project id as the
|
||||
self-contained `MetaGateway` app in this store.
|
||||
|
||||
## Why it is necessary
|
||||
|
||||
1. `10001:999` is the uid/gid the upstream meta-feeder-tmdb image runs its application as;
|
||||
its writable paths are baked to that owner. `/DATA/AppData/metagateway/card` is declared
|
||||
in `x-compose-app.folders` with `user: 10001` / `group: 999` so Maison creates it with
|
||||
the right ownership before the first start, instead of Compose creating it root-owned and
|
||||
the container failing on its first write.
|
||||
2. This is the point of the variant: on a box that already runs MetaCore and MetaShare,
|
||||
bundling a second Redis, leader-election tier and transport peer wastes memory and
|
||||
splits the record store in two.
|
||||
3. The two variants are alternatives, not companions — they share the project id and the
|
||||
same `/DATA/AppData/metagateway/` paths precisely so that installing one over the other
|
||||
reuses the same data rather than orphaning it. Exactly one may be installed at a time;
|
||||
this is stated in `tips.before_install`.
|
||||
|
||||
## Security mitigations in place
|
||||
|
||||
- Public ingress terminates at the AppShield sidecar (`ghcr.io/yundera/appshield:2.0.9`)
|
||||
behind the PCS's Authelia SSO. The backends carry no Caddy labels and publish no host
|
||||
port.
|
||||
- No user directories are mounted — no `/DATA/Documents`, `/DATA/Downloads`, `/DATA/Media`
|
||||
or `/DATA/Gallery`.
|
||||
- All binds are under `/DATA/AppData/metagateway/`, declared in `x-compose-app.folders`.
|
||||
- `cpu_shares` on every service; image tags pinned; no `:latest`.
|
||||
|
||||
## Alternatives considered and rejected
|
||||
|
||||
- **Run `metagateway-card` as `$PUID:$PGID`.** The upstream image's baked paths are owned by
|
||||
`10001:999`; it fails on first write.
|
||||
- **`chown` the directory in a hook instead.** The guidelines are explicit that a hook's
|
||||
`mkdir`/`chown` runs in the Maison container against a host path — the wrong filesystem.
|
||||
`folders` is the supported mechanism and it runs before the first pull.
|
||||
- **A distinct project id, so both variants can coexist.** They would then fight over the
|
||||
same public hostnames and the same record store; two gateways on one box is not a
|
||||
supported topology.
|
||||
|
||||
## Data protection
|
||||
|
||||
State is under `/DATA/AppData/metagateway/`, the unit Maison archives on uninstall and
|
||||
restores from backup. Because both MetaGateway variants use that path, switching between
|
||||
them preserves the user's configuration. The MetaCore and MetaShare data this variant reads
|
||||
belong to those apps and are archived with them.
|
||||
Reference in New Issue
Block a user