343 lines
17 KiB
YAML
343 lines
17 KiB
YAML
name: metashare
|
|
|
|
services:
|
|
# Backend service. Reachable on the pcs network as `metashare-app`.
|
|
# Public ingress goes through the `metashare` hash-lock proxy below.
|
|
#
|
|
# Unlike the other meta apps, this container is a self-contained Rust
|
|
# binary serving Axum on :3000 directly (no nginx wrapper, no supervisord).
|
|
# Ports 3000 and 4001 are split:
|
|
# - 3000 HTTP API + dashboard (proxied through the hash-lock + Caddy)
|
|
# - 4001 libp2p TCP listener — must be reachable on the public IP so
|
|
# other archivists' peers can dial in via kad-DHT provider records.
|
|
metashare-app:
|
|
# 1.0.13 = nocopy library seeding (meta-core files seeded as filestore refs,
|
|
# no second on-disk copy) + core/cache source surfacing in the dashboard, on
|
|
# top of the 1.0.8 floor this pin still requires: rsa-enabled libp2p
|
|
# (required to join the public IPFS DHT — the bootstrap.libp2p.io nodes are
|
|
# RSA-keyed; without `rsa` the noise handshake fails and this peer never
|
|
# actually reaches the DHT) + PUBLIC_ADDR support so remote archivists can
|
|
# dial this seeder.
|
|
image: ghcr.io/worph/meta-share:1.0.36
|
|
container_name: metashare-app
|
|
# 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: metashare-app
|
|
restart: unless-stopped
|
|
cpu_shares: 20
|
|
|
|
# Give meta-share room to shut down cleanly on SIGTERM (docker stop /
|
|
# `up -d` recreate) before docker escalates to SIGKILL. The binary catches
|
|
# SIGTERM, drains in-flight HTTP briefly, then unwinds so redb's Database
|
|
# Drop writes its allocator-state table — without which the next open runs a
|
|
# full multi-scan repair of blocks.redb (minutes of silent CPU before the
|
|
# port binds, proportional to blockstore size). 3s drain + the allocator
|
|
# write fits the 10s default, but a large store gets headroom here.
|
|
stop_grace_period: 30s
|
|
|
|
expose:
|
|
- 3000 # HTTP API (Axum)
|
|
|
|
# libp2p TCP must be host-published. Caddy can't proxy it (not HTTP)
|
|
# and other peers' kad-DHT lookups dial the public multiaddr directly.
|
|
ports:
|
|
- "4001:4001"
|
|
|
|
volumes:
|
|
# Shared infrastructure state (leader election, Redis dump, service discovery)
|
|
# Owned by MetaCore; mounted rw here so meta-share registers under services/.
|
|
# No /meta-core mount: this service locates meta-core over UDP
|
|
# (meta-discovery v1) instead of reading kv-leader.info off a shared
|
|
# volume. See docs/project-architecture/service-discovery.md.
|
|
# Operator settings (META_SHARE_CONFIG_DIR, default /config): settings.json
|
|
# holds the NNTP credentials + per-indexer API keys entered in the dashboard.
|
|
# MUST be persisted — without this mount they live on the container's
|
|
# writable layer and are lost on every image bump / recreate, forcing the
|
|
# operator to re-enter their credentials after each update.
|
|
- ${DATA_ROOT:-/DATA}/AppData/metashare/config:/config:rw
|
|
# Seeded bytes + the seed index (META_SHARE_DATA, default /data/meta-share):
|
|
# the redb IPFS blockstore, seed manifest, librqbit sparse torrent files
|
|
# (bt-temp/), the NZB cache and the ipfs-seed cursor. Recomputable, but a
|
|
# peer whose job is "fetch implies seed" comes back seeding NOTHING after a
|
|
# recreate without this, and re-downloads everything from a cold swarm.
|
|
- ${DATA_ROOT:-/DATA}/AppData/metashare/data:/data:rw
|
|
|
|
# Note: no /files mounts — meta-share resolves file paths through MetaCore's
|
|
# WebDAV URL (Architecture V3). META_SHARE_FILES_PATH is only used as the
|
|
# prefix for translating absolute filePaths back to WebDAV-relative paths.
|
|
|
|
environment:
|
|
- P2P_LISTEN=/ip4/0.0.0.0/tcp/4001
|
|
# Advertise the routable address (published host port 4001) so kad
|
|
# provider records + identify carry a dialable multiaddr — otherwise,
|
|
# behind Docker NAT, the swarm only sees container-internal addresses
|
|
# and remote archivists can't dial in to fetch seeded blocks.
|
|
# ${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 — meta-share REFUSES TO START rather than announce
|
|
# a lie (config.rs: "PUBLIC_ADDR is not a valid multiaddr").
|
|
- PUBLIC_ADDR=/ip4/${APP_PUBLIC_IPV4}/tcp/4001
|
|
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001. The default
|
|
# nested-dnsaddr form resolves to wss-only in some container resolvers
|
|
# (no ws transport → no bootstrap); these direct addresses avoid that.
|
|
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
|
|
# bootstrapper. Resilience: the four IPFS-Foundation bootstrappers can refuse
|
|
# an individual IP, and when they do a peer cannot join the DHT AT ALL.
|
|
# Observed on watch.nsl.sh 2026-09-01: 2 connections with the four alone,
|
|
# 126 once a reachable fifth was added.
|
|
- 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
|
|
|
|
# Service registration into /meta-core/services/
|
|
- BASE_URL=https://metashare-$APP_DOMAIN
|
|
|
|
# meta-core ingestion — the local archivist's library. This peer pulls
|
|
# records from MetaCore and announces them on the Kamilata swarm.
|
|
- META_CORE_URL=http://metacore-app:80
|
|
|
|
# Inter-peer fetch URL — where another archivist's meta-share reaches
|
|
# this peer's HTTP API. Same as BASE_URL for prod (other peers fetch
|
|
# via the public Caddy URL, which routes back through this hash-lock
|
|
# proxy with /api/peer/* and /api/file/* on the bypass list).
|
|
#
|
|
# Renamed at the 1.0.24 bump (2026-08-04), as the old note here said to do:
|
|
# META_SHARE_PEER_API_URL was the pre-1.0.8 spelling. watch.nsl.sh runs
|
|
# 1.0.24 with INTERNAL_BASE_URL and registers its peer_url correctly, so the
|
|
# deprecated name is no longer needed. Do NOT set both.
|
|
- INTERNAL_BASE_URL=https://metashare-$APP_DOMAIN
|
|
|
|
# NOTE: KAD_NAMESPACE, TARGET_PEER_COUNT, ENABLE_MDNS, META_CORE_POLL_SECS,
|
|
# META_CORE_MAX_RECORDS, HTTP_LISTEN, SERVICE_NAME and RUST_LOG were all
|
|
# dropped — each merely restated the code default in meta-share:1.0.8.
|
|
# Set KAD_NAMESPACE to a non-default value to isolate a private cohort
|
|
# (every peer in the cohort must agree on it).
|
|
|
|
- TZ=$TZ
|
|
|
|
# Runs as root because the meta-core volume is provisioned mode 0755 by
|
|
# MetaCore (root). Same convention as the other meta apps.
|
|
user: 0:0
|
|
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
networks:
|
|
- pcs
|
|
|
|
# OIDC perimeter for the MetaShare dashboard. Self-registers as an OIDC client
|
|
# with the platform's auth-registrar on first login (no secrets to manage).
|
|
#
|
|
# Naming: container_name MUST equal the URL-prefix of the public hostname so
|
|
# the registrar's redirect-URI allowlist accepts `metashare-<user>.<domain>`.
|
|
metashare:
|
|
image: ghcr.io/yundera/appshield:2.0.9
|
|
cpu_shares: 80
|
|
container_name: metashare
|
|
hostname: metashare
|
|
restart: unless-stopped
|
|
user: "0:0"
|
|
|
|
expose:
|
|
- 80
|
|
|
|
environment:
|
|
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
|
|
BACKEND_HOST: "metashare-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:
|
|
- metashare-app
|
|
|
|
labels:
|
|
# Peer-to-peer surface bypasses the hash-lock so other archivists'
|
|
# meta-share peers can reach /api/peers, /api/peer/*, /api/file/*,
|
|
# /api/search* and /api/services without an OIDC session. The
|
|
# dashboard (everything else) sits behind hash-lock as usual.
|
|
|
|
# ---- metashare-${APP_DOMAIN} (gateway TLS) ----
|
|
caddy_0: metashare-${APP_DOMAIN}
|
|
caddy_0.import: gateway_tls
|
|
caddy_0.0_handle: "/health"
|
|
caddy_0.0_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.1_handle: "/api/peers*"
|
|
caddy_0.1_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.2_handle: "/api/peer/*"
|
|
caddy_0.2_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.3_handle: "/api/file/*"
|
|
caddy_0.3_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.4_handle: "/api/search*"
|
|
caddy_0.4_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.5_handle: "/api/services"
|
|
caddy_0.5_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_0.6_handle: ""
|
|
caddy_0.6_handle.reverse_proxy: "{{upstreams 80}}"
|
|
|
|
# ---- metashare-IP.nip.io (gateway TLS) ----
|
|
caddy_1: metashare-${APP_PUBLIC_IP_DASH}.nip.io
|
|
caddy_1.import: gateway_tls
|
|
caddy_1.0_handle: "/health"
|
|
caddy_1.0_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.1_handle: "/api/peers*"
|
|
caddy_1.1_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.2_handle: "/api/peer/*"
|
|
caddy_1.2_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.3_handle: "/api/file/*"
|
|
caddy_1.3_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.4_handle: "/api/search*"
|
|
caddy_1.4_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.5_handle: "/api/services"
|
|
caddy_1.5_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_1.6_handle: ""
|
|
caddy_1.6_handle.reverse_proxy: "{{upstreams 80}}"
|
|
|
|
# ---- metashare-IP.sslip.io (Let's Encrypt) ----
|
|
caddy_2: metashare-${APP_PUBLIC_IP_DASH}.sslip.io
|
|
caddy_2.0_handle: "/health"
|
|
caddy_2.0_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.1_handle: "/api/peers*"
|
|
caddy_2.1_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.2_handle: "/api/peer/*"
|
|
caddy_2.2_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.3_handle: "/api/file/*"
|
|
caddy_2.3_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.4_handle: "/api/search*"
|
|
caddy_2.4_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.5_handle: "/api/services"
|
|
caddy_2.5_handle.reverse_proxy: "metashare-app:3000"
|
|
caddy_2.6_handle: ""
|
|
caddy_2.6_handle.reverse_proxy: "{{upstreams 80}}"
|
|
|
|
networks:
|
|
- pcs
|
|
|
|
networks:
|
|
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: |
|
|
**Decentralized metadata sharing across MetaMesh archivists.**
|
|
|
|
MetaShare turns your local MetaMesh library into a peer in a federated discovery network. Archivists running their own Yundera nodes can search across each other's libraries without surrendering control of their files — discovery returns pointers (peer + CID), and any retrieval is an explicit HTTP fetch from the source peer.
|
|
|
|
**Key Features:**
|
|
- **Federated Search** - Type-once, query-many across the cohort using the Kamilata routing primitive
|
|
- **Pointer-Only Discovery** - Search returns `(peer, cid)`; data never leaves the source until you ask for it
|
|
- **Automatic Peer Discovery** - Public IPFS DHT (kad) finds other archivists; mDNS handles same-LAN cohorts; no designated bootstrap node
|
|
- **Cohort Scoping** - `KAD_NAMESPACE` lets a private group of archivists isolate their swarm from the public default
|
|
- **Built on libp2p** - QUIC + TCP transport, Noise-encrypted, peer-id authenticated
|
|
- **MetaCore-Backed** - Ingests from your local MetaCore Redis and re-publishes records on the swarm
|
|
|
|
**Requires:** MetaCore (install first; MetaShare reads its metadata from MetaCore's API)
|
|
|
|
**Works with:** MetaSort (so MetaCore has records to share), MetaFuse, MetaStremio
|
|
|
|
**Status:** v0.2 POC. In-memory record store (rebuilt from MetaCore on each start), no persistent peer-id, no announcement signing yet. Production-grade hardening lands in v0.2.1.
|
|
|
|
**Dashboard Features:** Peer list with live connection status, federated search, per-record source attribution, swarm health diagnostics
|
|
|
|
fr_fr: |
|
|
**Partage de metadonnees decentralise entre archivistes MetaMesh.**
|
|
|
|
MetaShare transforme votre bibliotheque MetaMesh locale en pair d'un reseau de decouverte federe. Les archivistes faisant tourner leur propre serveur Yundera peuvent chercher dans les bibliotheques des autres sans abandonner le controle de leurs fichiers — la decouverte renvoie des pointeurs (pair + CID), et toute recuperation est une requete HTTP explicite vers le pair source.
|
|
|
|
**Fonctionnalites Cles:**
|
|
- **Recherche Federee** - Une seule requete interroge tout le cohorte via la primitive de routage Kamilata
|
|
- **Decouverte par Pointeurs** - La recherche renvoie `(pair, cid)` ; les donnees ne quittent le pair source que sur demande explicite
|
|
- **Decouverte Automatique des Pairs** - Le DHT IPFS public (kad) trouve les autres archivistes ; mDNS gere les cohortes sur le meme LAN ; aucun noeud bootstrap designe
|
|
- **Isolation par Cohorte** - `KAD_NAMESPACE` permet a un groupe prive d'archivistes d'isoler leur swarm du namespace public
|
|
- **Construit sur libp2p** - Transport QUIC + TCP, chiffrement Noise, authentification par peer-id
|
|
- **Adosse a MetaCore** - Ingere depuis votre Redis MetaCore local et republie les enregistrements sur le swarm
|
|
|
|
**Necessite:** MetaCore (installer en premier ; MetaShare lit ses metadonnees depuis l'API MetaCore)
|
|
|
|
**Fonctionne avec:** MetaSort (pour que MetaCore ait des enregistrements a partager), MetaFuse, MetaStremio
|
|
|
|
**Statut:** POC v0.2. Stockage des enregistrements en memoire (reconstruit depuis MetaCore a chaque demarrage), pas de peer-id persistant, pas encore de signature des annonces. Le durcissement de niveau production arrive en v0.2.1.
|
|
|
|
tagline:
|
|
en_us: Decentralized metadata sharing - Federated search across archivists without surrendering your files
|
|
fr_fr: Partage de metadonnees decentralise - Recherche federee entre archivistes sans abandonner vos fichiers
|
|
|
|
title:
|
|
en_us: MetaShare
|
|
|
|
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaShare/icon.png
|
|
screenshot_link:
|
|
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaShare/screenshot-1.png
|
|
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaShare/screenshot-2.png
|
|
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaShare/screenshot-3.png
|
|
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaShare/thumbnail.png
|
|
|
|
index: /
|
|
store_app_id: metashare
|
|
main: metashare
|
|
is_uncontrolled: false
|
|
|
|
tips:
|
|
before_install:
|
|
en_us: |
|
|
**Requires MetaCore** - Install MetaCore first.
|
|
|
|
**Port 4001 must be reachable from the public internet.** MetaShare's libp2p
|
|
TCP listener is host-published on port 4001 so other archivists' peers
|
|
(and the public IPFS kad-DHT) can dial this node directly. If your Yundera
|
|
host is behind a strict firewall, open TCP/4001 inbound — without it, this
|
|
peer can still *find* others but won't be findable by them.
|
|
|
|
**Cohort scoping (optional).** Out of the box, MetaShare joins the global
|
|
`metamesh-share-default` cohort. To run a private group of archivists,
|
|
change `KAD_NAMESPACE` to a shared secret string on every node in the
|
|
group — only nodes with the matching namespace will see each other.
|
|
|
|
**POC status.** v0.2 has an in-memory record store (rebuilt from MetaCore
|
|
on every restart, ~2.5s for ~500 records) and no announcement signing yet.
|
|
Don't rely on this for production federation until v0.2.1.
|
|
|
|
The dashboard is protected by Yundera's built-in Authelia via OIDC — sign
|
|
in with your existing Yundera account. The peer-to-peer API surface
|
|
(`/api/peers`, `/api/peer/*`, `/api/file/*`, `/api/search*`) is intentionally
|
|
bypassed so other peers can reach it without an OIDC session.
|
|
|
|
# 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/metacore/meta-core
|
|
user: $PUID
|
|
group: $PGID
|
|
- path: /DATA/AppData/metashare/config
|
|
user: $PUID
|
|
group: $PGID
|
|
- path: /DATA/AppData/metashare/data
|
|
user: $PUID
|
|
group: $PGID
|
|
# Same string as the caddy_0 label, so the tile link and the route agree.
|
|
webui-host: metashare-${domain}
|
|
webui-path: "/"
|