name: metaread # ============================================================================= # MetaRead — the READING client for the MetaMesh network. # # Comics and manga, books, papers and scanned documents. The third client on the # mesh after MetaWatch (video) and MetaListen (audio), and it is built the same # way: a self-contained stack with its OWN discovery (meta-search), transport # (meta-share) and meta-core, so it installs alongside MetaGateway and the # standalone Meta* apps with zero container-name / data-path collisions. # # metaread-core → /DATA/AppData/metaread/metacore (per-stack meta-core) # metaread-search → discovery (Kamilata federated search, identity proxy) # metaread-share → pure content transport (byte Range, bitswap, BT) # metaread-app → the reader UI/back-end (behind OIDC) # # Contrast with `MetaReadShared` in this store, which ships only meta-search and # reuses the standalone MetaCore + MetaShare apps (one profile shared with # MetaWatch). Pick exactly one: both use compose project `metaread` and the same # /DATA/AppData/metaread paths. # # ⚠ **Distinct libp2p host ports.** MetaWatch already publishes 4003/4004; this # stack uses 4005/4006. Two peers cannot share a host port, and the failure is # a container that will not start rather than anything subtle — but if you run # MetaListen too, keep walking the pairs upward. # # ── What it needs to actually show you something ───────────────────────────── # A client is a window onto the swarm; it holds no catalogue of its own. To see # comics you need a gateway peer running the literature feeders: # # **MetaFeeder · Suwayomi** — the chapters (and its own work cards), plus # optionally **MetaFeeder · AniList** to name the works. Install them beside # **MetaGateway** and register each feeder in the gateway's config. # # **MetaFeeder · Gutenberg** — Project Gutenberg (+ Open Library cards), for # public-domain books. # # Without either, MetaRead runs perfectly and shows an empty library — which is # the honest state of a mesh nobody has fed yet, not a fault. # ============================================================================= services: # --------------------------------------------------------------------------- # metaread-core — dedicated meta-core for this client: the identity keystore # (profiles are secp256k1 keypairs), the User Data Layer op-log, and whatever # records meta-search ingests off the swarm. File watcher OFF — MetaRead is a # consumer, not a library indexer. # --------------------------------------------------------------------------- metaread-core: image: ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/meta-core:1.0.24 container_name: metaread-core # Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM instead of # swap. mem_limit: 768m memswap_limit: 768m hostname: metaread-core restart: unless-stopped privileged: true user: "0:0" cpu_shares: 30 expose: - 9000 volumes: - ${DATA_ROOT:-/DATA}/AppData/metaread/metacore:/meta-core:rw environment: - BASE_URL=http://metaread-core:9000 - ENABLE_FILE_WATCHER=false - TZ=$TZ healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/health"] interval: 5s timeout: 3s retries: 30 start_period: 30s networks: - metaread # --------------------------------------------------------------------------- # metaread-search — DISCOVERY. Federated search (Kamilata Bloom routing), the # gateway fan-out, cover recovery over bitswap, the shared prefs and the User # Data Layer. Public libp2p on host port 4006; mDNS on `pcs` for same-host # peers. # # ⚠ This is the peer that reaches MetaGateway. An empty MetaRead library with # a healthy gateway usually means this peer never joined the DHT — check its # peer count before suspecting the feeders. # --------------------------------------------------------------------------- metaread-search: image: ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/meta-search:1.2.10 container_name: metaread-search mem_limit: 1g memswap_limit: 1g hostname: metaread-search restart: unless-stopped user: "0:0" cpu_shares: 30 expose: - 3000 ports: - "4006:4001" volumes: # No /meta-core mount: this service locates meta-core over UDP # (beacon v2) instead of reading kv-leader.info off a shared # volume. See docs/project-architecture/beacon-v2.md. - ${DATA_ROOT:-/DATA}/AppData/metaread/search-data:/data:rw environment: P2P_LISTEN: "/ip4/0.0.0.0/tcp/4001" # ⚠ The IPV4 spelling, never the bare APP_PUBLIC_IP: on an IPv6-primary # PCS that variable holds the v6 address, and `/ip4//tcp/...` is not a # parseable multiaddr — the peer REFUSES TO START rather than announce a # lie. PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4006" # Explicit /dns4/ bootstraps: the nested dnsaddr default resolves to # wss-only in some container resolvers. mars.i.ipfs.io is the only # surviving legacy *.i.ipfs.io bootstrapper and is here for resilience — # the four IPFS-Foundation nodes 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). Measured 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" META_CORE_URL: "http://metaread-core:9000" BASE_URL: "http://metaread-search:3000" INTERNAL_BASE_URL: "http://metaread-search:3000" TZ: "$TZ" depends_on: metaread-core: condition: service_healthy healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:3000/health"] interval: 5s timeout: 3s retries: 10 start_period: 5s networks: - metaread - pcs # --------------------------------------------------------------------------- # metaread-share (+ metaread-share-ipfs) — CONTENT TRANSPORT: the meta-share # hull with one ipfs plugin. Fetches and serves bytes by CID over bitswap, and # seeds whatever it fetches. Public libp2p on host port 4005. # # ⚠ It must be reachable by whatever holds the pages. A comic chapter from the # aggregator tier is a list of per-page URL locators redeemed HERE, so a # Suwayomi behind auth or on a LAN this peer cannot reach yields a chapter # that lists correctly and will not open. # --------------------------------------------------------------------------- metaread-share: # The meta-share HULL (2.x): HTTP API, material index, seed manifest, # eviction, playback focus, meta-core ingest. Every byte-moving protocol is # a transport plugin; this bundle ships ONE — metaread-share-ipfs below — which # makes it the read cache: page bytes over bitswap + re-seed (a chapter is tens of MB, never a film). Torrent and local NZB tiers are not bundled # (META_SHARE_PLUGIN_{TORRENT,NZB}_URL=off keeps the hull from seeding the # MetaShare app's `metashare-` hostnames, which on a shared `pcs` # would be ANOTHER hull's plugins writing to another /data). # - 3000 HTTP API - 3001 plugin callback API (internal only) image: ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/meta-share:2.6.4 container_name: metaread-share # The swarm and blockstore moved to the ipfs plugin, so the hull needs far # less than the monolith did. mem_limit: 1g memswap_limit: 1g hostname: metaread-share restart: unless-stopped user: "0:0" cpu_shares: 30 # material.redb writes its allocator state on Drop; give SIGTERM room. stop_grace_period: 30s expose: - 3000 - 3001 volumes: # settings.json — network endpoints and the plugin list. - ${DATA_ROOT:-/DATA}/AppData/metaread/metashare-config:/config:rw # The shared data volume: cache/, tmp/, seeds.json, material.redb are the # hull's; ipfs/blocks.redb is the ipfs plugin's corner of the SAME folder # (layout unchanged from 1.0.x, so an existing cache carries over). - ${DATA_ROOT:-/DATA}/AppData/metaread/share-data:/data:rw environment: META_CORE_URL: "http://metaread-core:9000" BASE_URL: "http://metaread-share:3000" INTERNAL_BASE_URL: "http://metaread-share:3000" META_SHARE_PLUGIN_IPFS_URL: "http://metaread-share-ipfs:3000" META_SHARE_PLUGIN_TORRENT_URL: "off" META_SHARE_PLUGIN_NZB_URL: "off" TZ: "$TZ" depends_on: metaread-core: condition: service_healthy healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:3000/health"] interval: 5s timeout: 3s retries: 10 start_period: 10s networks: - metaread - pcs metaread-share-ipfs: # The hull's only transport plugin: the libp2p swarm (kad-DHT, identify, # mDNS, bitswap), the blockstore, gateway discovery and the peer directory # the hull uses to forward Usenet cids. Waits up to 60 s for the hull's # internal /health before opening blocks.redb, so no depends_on. image: ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/meta-transport-ipfs:1.3.4 container_name: metaread-share-ipfs hostname: metaread-share-ipfs restart: unless-stopped user: "0:0" cpu_shares: 30 mem_limit: 1500m memswap_limit: 1500m stop_grace_period: 30s expose: - 3000 # libp2p TCP must be host-published (kad lookups dial it directly; Caddy # cannot proxy it). Same host port the 1.0.x monolith used. ports: - "4005:4001" volumes: - ${DATA_ROOT:-/DATA}/AppData/metaread/share-data:/data:rw - ${DATA_ROOT:-/DATA}/AppData/metaread/share-ipfs-state:/state:rw environment: META_SHARE_HULL_URL: "http://metaread-share:3001" # Beacon v2: only this bundle's hull may list this plugin. BEACON_BINDS: "metaread-share" # ${APP_PUBLIC_IPV4}, NOT ${APP_PUBLIC_IP}: on an IPv6-primary PCS the # latter is a v6 address, and /ip4//… is not a parseable multiaddr. PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4005" # Public IPFS DHT bootstraps by explicit /dns4/…/tcp/4001, plus # mars.i.ipfs.io in case the foundation four refuse this IP. 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" healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:3000/health"] interval: 10s timeout: 3s retries: 10 start_period: 30s networks: - metaread - pcs # --------------------------------------------------------------------------- # metaread-app — the reader. Zero page work server-side: a flat Range/206 byte # proxy to metaread-share, with the CBZ/EPUB/PDF decoding done in the browser. # My List, Continue Reading and reading positions live in the User Data Layer. # --------------------------------------------------------------------------- metaread-app: image: ipcr-watch.nsl.sh/ipns/metadec.eth/metadec.eth/meta-read:1.3.9.1 container_name: metaread-app hostname: metaread-app restart: unless-stopped # The image's own USER is uid 10001, but the data dir is created as # $PUID:$PGID by x-compose-app `folders`, so the stock user cannot write # library.json, the session key or the tile cache. Pin the runtime uid to # the one that owns the mount, as metawatch-app does. user: "1000:1000" cpu_shares: 30 mem_limit: 512m memswap_limit: 512m expose: - 3000 volumes: - ${DATA_ROOT:-/DATA}/AppData/metaread/data:/app/data environment: META_SHARE_URL: "http://metaread-share:3000" META_SEARCH_URL: "http://metaread-search:3000" # Browser-reachable topbar chips — the AppShield gates below expose the # co-located transport and discovery peers behind Yundera OIDC. META_SHARE_PUBLIC_URL: "https://metareadshare-${APP_DOMAIN}" META_SEARCH_PUBLIC_URL: "https://metareadsearch-${APP_DOMAIN}" # Standalone: this app brings its own meta-core and there is no MetaWatch to # create profiles in, so profile creation stays invite-gated here. META_READ_PROFILE_CREATION: "local" TZ: "$TZ" depends_on: metaread-search: condition: service_healthy metaread-share: condition: service_healthy healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:3000/health"] interval: 5s timeout: 3s retries: 10 start_period: 5s networks: - metaread # --------------------------------------------------------------------------- # OIDC perimeter for the reader UI. container_name MUST equal the URL prefix # so the registrar's redirect-URI allowlist matches `metaread-.`. # --------------------------------------------------------------------------- metaread: image: ghcr.io/yundera/appshield:2.0.9 cpu_shares: 80 container_name: metaread hostname: metaread 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}.${APP_IP_ZONE},${APP_PUBLIC_IP_DASH}.sslip.io" BACKEND_HOST: "metaread-app" BACKEND_PORT: "3000" LISTEN_PORT: "80" labels: caddy_0: metaread-${APP_DOMAIN} caddy_0.import: gateway_tls caddy_0.reverse_proxy: "{{upstreams 80}}" caddy_1: metaread-${APP_PUBLIC_IP_DASH}.${APP_IP_ZONE} caddy_1.import: gateway_tls caddy_1.reverse_proxy: "{{upstreams 80}}" caddy_2: metaread-${APP_PUBLIC_IP_DASH}.sslip.io caddy_2.reverse_proxy: "{{upstreams 80}}" depends_on: - metaread-app networks: - metaread - pcs # --------------------------------------------------------------------------- # Gates for the two co-located peers, so the reader's topbar chips resolve to # something a browser can open. Diagnostics only — nothing routes through them. # --------------------------------------------------------------------------- metareadshare: image: ghcr.io/yundera/appshield:2.0.9 cpu_shares: 50 container_name: metareadshare hostname: metareadshare 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}.${APP_IP_ZONE},${APP_PUBLIC_IP_DASH}.sslip.io" BACKEND_HOST: "metaread-share" BACKEND_PORT: "3000" LISTEN_PORT: "80" labels: caddy_0: metareadshare-${APP_DOMAIN} caddy_0.import: gateway_tls caddy_0.reverse_proxy: "{{upstreams 80}}" caddy_1: metareadshare-${APP_PUBLIC_IP_DASH}.${APP_IP_ZONE} caddy_1.import: gateway_tls caddy_1.reverse_proxy: "{{upstreams 80}}" caddy_2: metareadshare-${APP_PUBLIC_IP_DASH}.sslip.io caddy_2.reverse_proxy: "{{upstreams 80}}" depends_on: - metaread-share networks: - metaread - pcs metareadsearch: image: ghcr.io/yundera/appshield:2.0.9 cpu_shares: 50 container_name: metareadsearch hostname: metareadsearch 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}.${APP_IP_ZONE},${APP_PUBLIC_IP_DASH}.sslip.io" BACKEND_HOST: "metaread-search" BACKEND_PORT: "3000" LISTEN_PORT: "80" labels: caddy_0: metareadsearch-${APP_DOMAIN} caddy_0.import: gateway_tls caddy_0.reverse_proxy: "{{upstreams 80}}" caddy_1: metareadsearch-${APP_PUBLIC_IP_DASH}.${APP_IP_ZONE} caddy_1.import: gateway_tls caddy_1.reverse_proxy: "{{upstreams 80}}" caddy_2: metareadsearch-${APP_PUBLIC_IP_DASH}.sslip.io caddy_2.reverse_proxy: "{{upstreams 80}}" depends_on: - metaread-search networks: - metaread - pcs networks: metaread: name: metaread driver: bridge pcs: name: ${APP_NET:-pcs} external: true x-casaos: architectures: - amd64 - arm64 author: Worph Team category: Media developer: Aptero Team main: metaread webui_port: 80 index: / description: en_us: | **A reading client for the MetaMesh network — comics and manga, books, papers and scanned documents.** MetaRead is to reading what **MetaWatch** is to video: a library you browse by *work* rather than by filename. Search returns a grid of covers, a work opens to its chapter or edition list, and a chapter opens in a reader that does the page decoding **in your browser** — the server is a flat byte proxy and never unpacks an archive on your behalf. **What it reads.** CBZ and CBR comics, EPUB and PDF books, and the per-page image lists that a manga aggregator serves for a chapter nobody has downloaded yet. Reading direction, page layout and spread are per-work and remembered. **How the catalogue works.** The depth of the hierarchy is a property of the item, not of the app. A manga is *work → volume → chapter*, with several scanlations per chapter and a chooser to pick between them. A Gutenberg novel is one work and one file — its page *is* the reader entry point, with no synthetic chapter 1 invented to pad it out. **Languages.** Pick the languages you read once, in the top bar, and every view honours it — the same preference MetaWatch and the meta-share dashboard already speak. An item that declares no language is always shown: absence means *unknown*, never *not yours*. **Self-contained.** Ships its own discovery, transport and meta-core, so it installs beside **MetaGateway** and the other Meta* apps with no collisions. Your profile is a keypair, and your list, positions and ratings live in the User Data Layer rather than on someone's server. **Requires a feeder to have anything to show:** **MetaFeeder · Suwayomi** (with **MetaFeeder · AniList**) for comics and manga, **MetaFeeder · Gutenberg** for public-domain books. Without one it runs perfectly and shows an empty library — the honest state of a mesh nobody has fed. fr_fr: | **Un client de lecture pour le réseau MetaMesh — BD et manga, livres, articles et documents numérisés.** MetaRead est à la lecture ce que **MetaWatch** est à la vidéo : une bibliothèque que l'on parcourt par *œuvre* plutôt que par nom de fichier. La recherche renvoie une grille de couvertures, une œuvre s'ouvre sur sa liste de chapitres ou d'éditions, et un chapitre s'ouvre dans un lecteur qui décode les pages **dans votre navigateur** — le serveur n'est qu'un relais d'octets et n'ouvre jamais une archive à votre place. **Ce qu'il lit.** BD en CBZ et CBR, livres en EPUB et PDF, et les listes d'images par page qu'un agrégateur de manga sert pour un chapitre que personne n'a encore téléchargé. Sens de lecture, mise en page et double page sont propres à chaque œuvre et mémorisés. **La profondeur du catalogue appartient à l'œuvre, pas à l'app.** Un manga est *œuvre → volume → chapitre*, avec plusieurs scanlations par chapitre et un sélecteur pour choisir. Un roman de Gutenberg est une œuvre et un fichier — sa page *est* le point d'entrée du lecteur, sans chapitre 1 artificiel inventé pour meubler. **Langues.** Choisissez une fois les langues que vous lisez, dans la barre du haut, et toutes les vues la respectent. Un élément sans langue déclarée reste affiché : l'absence signifie *inconnu*, jamais *pas pour vous*. **Autonome.** Embarque sa propre découverte, son transport et son meta-core. Votre profil est une paire de clés ; listes, positions et notes vivent dans la User Data Layer. **Nécessite un feeder** pour avoir quelque chose à afficher : **MetaFeeder · Suwayomi** (avec **MetaFeeder · AniList**) pour la BD et le manga, **MetaFeeder · Gutenberg** pour les livres du domaine public. tagline: en_us: Read comics, manga and books from the MetaMesh network fr_fr: Lisez BD, manga et livres depuis le réseau MetaMesh title: en_us: MetaRead icon: https://gitea-watch.nsl.sh/metastar/MetaAppStore/raw/branch/main/Apps/MetaRead/icon.png thumbnail: https://gitea-watch.nsl.sh/metastar/MetaAppStore/raw/branch/main/Apps/MetaRead/thumbnail.png store_app_id: metaread is_uncontrolled: false tips: before_install: en_us: | **Ports.** This stack publishes libp2p on host ports **4005** and **4006**. MetaWatch already uses 4003/4004 — if you run another Meta* client, keep walking the pairs upward. Two peers cannot share a host port. **Install order:** 1. **MetaGateway** — the peer that runs the feeders and announces what they find. 2. **MetaFeeder · Suwayomi** (+ **MetaFeeder · AniList**) and/or **MetaFeeder · Gutenberg**, then register each in the gateway's *Add a feeder* box. Gateway feeder discovery is **startup-only**, so the gateway restarts itself afterwards. 3. This app. **First run.** Open MetaRead and create a profile — it is a keypair generated in your browser, and the secret key is shown once. Save it: it is the only way back in, and nobody can reset it for you. **An empty library is not a fault.** MetaRead holds no catalogue of its own. If the grid is empty, the question is whether a feeder is attached and whether this stack's discovery peer has joined the DHT — the *meta-search* chip in the top bar shows its peer count. **Authentication:** the UI is behind Yundera's Authelia (OIDC), and your reading profile is a second, separate identity inside it. x-compose-app: schema_version: 2 folders: - path: /DATA/AppData/metaread/metacore user: $PUID group: $PGID - path: /DATA/AppData/metaread/data user: $PUID group: $PGID - path: /DATA/AppData/metaread/share-data user: $PUID group: $PGID - path: /DATA/AppData/metaread/share-ipfs-state user: $PUID group: $PGID - path: /DATA/AppData/metaread/search-data user: $PUID group: $PGID - path: /DATA/AppData/metaread/metashare-config user: $PUID group: $PGID webui-host: metaread-${domain} webui-path: "/"