app: id: cuprate name: Cuprate # Matches the crate's own Cargo.toml version (binaries/cuprated/Cargo.toml). # Cuprate has no stable release yet — this is explicitly work-in-progress # software (see upstream README). The image tag below pins the exact # commit built, since "0.1.0-preview" alone is not reproducible. version: 0.1.0-preview # Where this app comes from, so scripts/check-upstream-releases.py can # tell us when the pin below has fallen behind. Without it nothing can: # container.image names our mirror, not the project it was mirrored from. upstream: kind: github repo: Cuprate/cuprate description: Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network. category: money container: # Built from the upstream Dockerfile at the tip of main, 18 commits past # the cuprated-0.1.0-preview tag (commit 618ff14, 2026-08-19) — there is # no newer tagged release as of this writing. Re-pin to a tagged release # once upstream cuts one. image: source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14 pull_policy: if-not-present network: archy-net # The image's own ENTRYPOINT is ["/usr/local/bin/cuprated"]; these are # appended as its argv, matching the project's own systemd unit # (cuprated.service) invocation exactly. custom_args: ["--config-file", "/home/cuprate/Cuprated.toml"] # The image (FROM scratch) creates uid:gid 1000:1000 for the `cuprate` # user at build time and runs as it unconditionally (USER 1000:1000, # no shell to switch users at runtime) — same pattern as # apps/phoenixd, apps/electrumx, apps/nostr-rs-relay, apps/portainer, # apps/barkd. The bind-mounted data dir must be owned by that literal # uid or cuprated dies on a permission error the first time it writes. data_uid: "1000:1000" dependencies: # Monero mainnet is ~250GiB unpruned as of 2026 and growing ~60GiB/year. # Verified against upstream main (binaries/cuprated/src/config.rs, 2026-09): # cuprated has NO on-disk pruning setting of any kind — the `pruning` # crate in its workspace is Monero's p2p *protocol* pruning, not a # smaller chain — so unlike bitcoin-knots this app CANNOT self-prune # when disk is scarce (see the DISK_GB branch in # apps/bitcoin-knots/manifest.yml). Left running on a too-small disk it # syncs until the filesystem fills and takes Archipelago down. The # disk-scarce equivalent is enforced in Rust instead: install/start # refuse, and boot reconcile skips, on any node under # CUPRATE_MIN_DISK_GB (450GB — chain + headroom; refuses the 250GB VPS # class, allows 500GB-class disks). If upstream ever ships a prune flag, # replace that gate with the bitcoin-style entrypoint branch. - storage: 300Gi resources: cpu_limit: 0 # Raised from 4Gi alongside target_max_memory below (see files[] comment) # — 2026-09-03 incident: a 4Gi/3GB-cache config starved # cuprated's DB cache into constant eviction/flush, driving 45% sustained # CPU and ~595GB/24h of block I/O on a fully-synced node. 10Gi leaves # headroom above the 8GiB cache for the process itself. memory_limit: 10Gi disk_limit: 300Gi security: # FROM scratch, no package manager/shell, ownership fixed at build time # — unlike bitcoin-knots this needs no runtime chown/setuid dance, so it # can run fully read-only with an empty capability set. capabilities: [] readonly_root: true no_new_privileges: true network_policy: isolated ports: # P2P. Cuprate's own default listen address is already 0.0.0.0 # (p2p.clear_net.listen_on), so no config override is needed — only the # host-side port differs from Monero's canonical 18080 because that # number is already taken on this fleet by lnd's REST port. - host: 18183 container: 18080 protocol: tcp auth: none auth_rationale: >- Monero p2p gossip. Peers are anonymous by design and speak the Monero wire protocol, not HTTP. # Unrestricted RPC (full node control) is deliberately NOT published. # cuprated has no RPC authentication, and for a published port to reach # it the service would have to bind 0.0.0.0 inside the container — at # which point every other app can reach it directly on 18081, since # ports[].bind only restricts the HOST side and podman bridges route to # each other (verified live 2026-08-22: a peer container on archy-net # got an unauthenticated get_info, from a *different* network). That is # unlike bitcoin-knots, whose 0.0.0.0 RPC still demands the rpcuser / # rpcpassword it writes from generated secrets. So unrestricted RPC is # left at cuprated's own default — container loopback only, reachable by # nothing — which is also what upstream intends by refusing a non-local # bind without an explicit i_know_what_im_doing override. # Restricted RPC: Monero's own purpose-built safe-for-public subset — # what wallets use when connecting to a "remote node". Disabled by # cuprated's own default; enabled via files[] below. `open`, not `gated`: # the gate still takes the port over (loopback pin, external binds, # fronts the Tor onion) but skips the dashboard login challenge, same # reasoning as electrumx's port — wallet clients (Feather, # monero-wallet-rpc, GUI) speak plain HTTP JSON-RPC programmatically and # cannot complete a browser login or hold a session cookie. The daemon # still uses its canonical container port 18089, but Penpot already owns # host port 18089, so this maps the public host port to the free 18090 # instead. - host: 18090 container: 18089 protocol: tcp auth: open auth_rationale: >- Monero restricted RPC — the subset upstream considers safe for public/remote-node use. Wallets (Feather, monero-wallet-rpc, GUI) connect directly over plain HTTP JSON-RPC and cannot complete a browser login or hold a dashboard session cookie. volumes: - type: bind source: /var/lib/archipelago/cuprate target: /home/cuprate options: [rw] # Settings that need to differ from cuprated's own documented defaults # (verified against `cuprated --generate-config` and `--dry-run` locally, # 2026-08-21): # - fast_sync: cuprated's own default is false, which performs full # cryptographic verification (ring signatures + RandomX PoW) on every # incoming block instead of trusting checkpointed history. Root-caused # 2026-09-03 as the dominant cause of a sustained 45% CPU node, # vs. 2.8% on a reference node with fast_sync = true — same chain height, same # block rate. Set explicitly rather than relying on the binary # default so fresh deploys don't silently regress into full-verify. # - target_max_memory: cuprated's own default auto-detects total *host* # RAM via sysinfo, which inside a memory-limited container would let # it size caches far past what resources.memory_limit above actually # grants — same class of problem bitcoin-knots' -dbcache sizing # comment addresses. Set explicitly, comfortably under the 10Gi limit. # Previously 3000000000 (~2.8GiB); that starved the DB cache and # forced constant eviction/flush (595GB/24h block I/O on a node just # appending ~2MB blocks every 2 minutes) — raised to 8GiB, matching # the healthy reference node, and # resources.memory_limit above raised in step to keep headroom above it. # - rpc.restricted.enable: cuprated ships this off by default; flip on # so the auth:none host port above actually serves something instead # of refusing every connection. port stays at its documented default # (canonical 18089), and advertise stays false — this node is not # opting in to being listed as a public remote node over the p2p # network, just reachable if someone points a wallet at it directly. # - rpc.unrestricted.address + the allow-public flag: cuprated's own # default (127.0.0.1) looks like the obviously-correct choice for a # port meant to stay loopback-only, but verified live (2026-08-21) # that a service bound literally to 127.0.0.1 *inside* the container # is unreachable through the host's published port — connections # reset regardless of how long the daemon has been up. Binding # 0.0.0.0 inside and letting ports[].bind: 127.0.0.1 below be the # actual restriction is the same pattern apps/bitcoin-knots already # uses for its own RPC port (-rpcbind=0.0.0.0:8332 internally, gate # restricts it externally) — not a new risk, the same one already # reviewed and accepted for Bitcoin's RPC. # - tracing.stdout.level / tracing.file.{level,max_log_files}: an # operator reading Cuprated.toml on disk should be able to see and # tune the log level directly instead of the file silently omitting # the whole [tracing] table (verified live on the affected node # 2026-09-01: the deployed file had no [tracing] section at all, and # the level was only discoverable by running `cuprated # --generate-config` and diffing). file.level is set to "info", NOT # cuprated's own raw default of "debug" — matches the reference dev # config this app was built and tested against (verified 2026-09-01), # which deliberately runs file logging quieter # than the binary default. max_log_files similarly follows that # reference (14, not the binary default of 7). files: - path: /var/lib/archipelago/cuprate/Cuprated.toml content: | network = "Mainnet" fast_sync = true target_max_memory = 8589934592 [rpc.restricted] enable = true [tracing.stdout] level = "info" [tracing.file] level = "info" max_log_files = 14 overwrite: false health_check: type: tcp # Restricted RPC — the only RPC surface published now. endpoint: localhost:18090 interval: 30s timeout: 5s retries: 3 start_period: 5m metadata: icon: /assets/img/app-icons/cuprate.svg category: money tier: optional author: Cuprate repo: https://github.com/Cuprate/cuprate