diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json
index 68e55d82..e55d4b0d 100644
--- a/app-catalog/catalog.json
+++ b/app-catalog/catalog.json
@@ -14,7 +14,7 @@
"id": "bitcoin-knots",
"title": "Bitcoin Knots",
"version": "28.1.0",
- "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.",
+ "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
"author": "Bitcoin Knots",
"category": "money",
@@ -25,8 +25,8 @@
{
"id": "bitcoin-core",
"title": "Bitcoin Core",
- "version": "28.4",
- "description": "Reference Bitcoin node implementation. Alternative to Bitcoin Knots; uninstall Knots before switching.",
+ "version": "28.4.0",
+ "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-core.svg",
"author": "Bitcoin Core contributors",
"category": "money",
@@ -38,7 +38,7 @@
"id": "lnd",
"title": "LND",
"version": "0.18.4",
- "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.",
+ "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
"icon": "/assets/img/app-icons/lnd.svg",
"author": "Lightning Labs",
"category": "money",
@@ -53,7 +53,7 @@
"id": "btcpay-server",
"title": "BTCPay Server",
"version": "2.3.9",
- "description": "Self-hosted Bitcoin payment processor.",
+ "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
"icon": "/assets/img/app-icons/btcpay-server.png",
"author": "BTCPay Server Foundation",
"category": "commerce",
@@ -76,8 +76,17 @@
"dockerImage": "ghcr.io/saleor/saleor:3.23",
"repoUrl": "https://github.com/saleor/saleor",
"containerConfig": {
- "ports": ["9011:80", "9010:80", "8000:8000", "8025:8025", "16686:16686"],
- "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"],
+ "ports": [
+ "9011:80",
+ "9010:80",
+ "8000:8000",
+ "8025:8025",
+ "16686:16686"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/saleor:/app/media",
+ "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"
+ ],
"notes": "Installed as a Saleor stack: customer storefront on 9011, admin dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor."
}
},
@@ -85,7 +94,7 @@
"id": "mempool",
"title": "Mempool Explorer",
"version": "3.0.0",
- "description": "Self-hosted Bitcoin blockchain and mempool visualizer.",
+ "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
"icon": "/assets/img/app-icons/mempool.webp",
"author": "Mempool",
"category": "money",
@@ -101,7 +110,7 @@
"id": "electrumx",
"title": "ElectrumX",
"version": "1.18.0",
- "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.",
+ "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
"icon": "/assets/img/app-icons/electrumx.png",
"author": "Luke Childs",
"category": "money",
@@ -116,7 +125,7 @@
"id": "indeedhub",
"title": "IndeeHub",
"version": "1.0.0",
- "description": "Bitcoin documentary streaming with Nostr identity.",
+ "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
"icon": "/assets/img/app-icons/indeedhub.png",
"author": "IndeeHub",
"category": "community",
@@ -127,49 +136,133 @@
"id": "botfights",
"title": "BotFights",
"version": "1.1.0",
- "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.",
+ "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.",
"icon": "/assets/img/app-icons/botfights.svg",
"author": "BotFights",
"category": "community",
"dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0",
"repoUrl": "https://botfights.net",
"containerConfig": {
- "ports": ["9100:9100"],
- "volumes": ["/var/lib/archipelago/botfights:/app/server/data"],
- "env": ["NODE_ENV=production", "PORT=9100", "FIGHT_LOOP_ENABLED=true", "ARCHY_EMBEDDED=1"]
+ "ports": [
+ "9100:9100"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/botfights:/app/server/data"
+ ],
+ "env": [
+ "NODE_ENV=production",
+ "PORT=9100",
+ "FIGHT_LOOP_ENABLED=true",
+ "ARCHY_EMBEDDED=1"
+ ]
}
},
{
"id": "gitea",
"title": "Gitea",
"version": "1.23",
- "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.",
+ "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
"icon": "/assets/img/app-icons/gitea.svg",
"author": "Gitea",
"category": "development",
- "dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23",
+ "dockerImage": "docker.io/gitea/gitea:1.23",
"repoUrl": "https://gitea.com",
"containerConfig": {
- "ports": ["3001:3000", "2222:22"],
- "volumes": ["/var/lib/archipelago/gitea/data:/data", "/var/lib/archipelago/gitea/config:/etc/gitea"],
- "env": ["GITEA__database__DB_TYPE=sqlite3", "GITEA__server__SSH_PORT=2222", "GITEA__server__SSH_LISTEN_PORT=22", "GITEA__server__LFS_START_SERVER=true", "GITEA__packages__ENABLED=true", "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", "GITEA__security__X_FRAME_OPTIONS="]
- }
+ "ports": [
+ "3001:3000",
+ "2222:22"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/gitea/data:/data",
+ "/var/lib/archipelago/gitea/config:/etc/gitea"
+ ],
+ "env": [
+ "GITEA__database__DB_TYPE=sqlite3",
+ "GITEA__server__SSH_PORT=2222",
+ "GITEA__server__SSH_LISTEN_PORT=22",
+ "GITEA__server__LFS_START_SERVER=true",
+ "GITEA__packages__ENABLED=true",
+ "GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
+ "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
+ "GITEA__security__X_FRAME_OPTIONS="
+ ]
+ },
+ "tier": "optional"
},
{
"id": "filebrowser",
"title": "File Browser",
"version": "2.27.0",
- "description": "Web-based file manager.",
+ "description": "Baseline Archipelago file manager service.",
"icon": "/assets/img/app-icons/file-browser.webp",
"author": "File Browser",
"category": "data",
"tier": "core",
- "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0",
+ "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0",
"repoUrl": "https://github.com/filebrowser/filebrowser",
"containerConfig": {
- "ports": ["8083:80"],
- "volumes": ["/var/lib/archipelago/filebrowser:/srv", "/var/lib/archipelago/filebrowser-data:/data"],
- "args": ["--database=/data/database.db", "--root=/srv", "--address=0.0.0.0", "--port=80"]
+ "ports": [
+ "8083:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/filebrowser:/srv",
+ "/var/lib/archipelago/filebrowser-data:/data"
+ ],
+ "args": [
+ "--database=/data/database.db",
+ "--root=/srv",
+ "--address=0.0.0.0",
+ "--port=80"
+ ]
+ }
+ },
+ {
+ "id": "nostr-rs-relay",
+ "title": "Nostr Relay (Rust)",
+ "version": "0.8.0",
+ "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
+ "icon": "/assets/img/app-icons/nostr.svg",
+ "author": "Nostr RS Relay",
+ "category": "community",
+ "tier": "recommended",
+ "dockerImage": "scsibug/nostr-rs-relay:0.8.9",
+ "repoUrl": "https://github.com/scsibug/nostr-rs-relay",
+ "containerConfig": {
+ "ports": [
+ "8081:8080"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/nostr-relay:/usr/src/app/db"
+ ],
+ "env": [
+ "RELAY_NAME=Archipelago Nostr Relay",
+ "RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
+ ]
+ }
+ },
+ {
+ "id": "meshtastic",
+ "title": "Meshtastic",
+ "version": "2-daily-alpine",
+ "description": "Open-source mesh networking for LoRa radios. Create decentralized communication networks.",
+ "icon": "/assets/img/app-icons/meshcore.svg",
+ "author": "Meshtastic",
+ "category": "networking",
+ "tier": "recommended",
+ "dockerImage": "docker.io/meshtastic/meshtasticd:daily-alpine",
+ "repoUrl": "https://github.com/meshtastic/firmware",
+ "containerConfig": {
+ "ports": [
+ "4403:4403"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/meshtastic:/var/lib/meshtasticd"
+ ],
+ "env": [
+ "MESHTASTIC_PORT=/dev/ttyUSB0",
+ "MESHTASTIC_SERIAL=true"
+ ],
+ "notes": "Requires a LoRa radio device at /dev/ttyUSB0. The config file is rendered from the app manifest before container start."
}
},
{
@@ -184,15 +277,19 @@
"dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine",
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
"containerConfig": {
- "ports": ["8082:80"],
- "volumes": ["/var/lib/archipelago/vaultwarden:/data"]
+ "ports": [
+ "8082:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/vaultwarden:/data"
+ ]
}
},
{
"id": "searxng",
"title": "SearXNG",
- "version": "2024.1.0",
- "description": "Privacy-respecting metasearch engine.",
+ "version": "1.0.0",
+ "description": "Privacy-respecting metasearch engine. Search the web without tracking.",
"icon": "/assets/img/app-icons/searxng.png",
"author": "SearXNG",
"category": "data",
@@ -200,21 +297,46 @@
"dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest",
"repoUrl": "https://github.com/searxng/searxng",
"containerConfig": {
- "ports": ["8888:8080"],
- "volumes": ["/var/lib/archipelago/searxng:/etc/searxng"]
+ "ports": [
+ "8888:8080"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/searxng:/etc/searxng"
+ ]
}
},
{
"id": "fedimint",
"title": "Fedimint",
"version": "0.10.0",
- "description": "Federated Bitcoin mint with privacy through federated guardians.",
+ "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.",
"icon": "/assets/img/app-icons/fedimint.png",
"author": "Fedimint",
"category": "money",
"dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0",
"repoUrl": "https://github.com/fedimint/fedimint"
},
+ {
+ "id": "fedimint-gateway",
+ "title": "Fedimint Gateway",
+ "version": "0.10.0",
+ "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.",
+ "icon": "/assets/img/app-icons/fedimint.png",
+ "author": "Fedimint",
+ "category": "money",
+ "dockerImage": "git.tx1138.com/lfg2025/gatewayd:v0.10.0",
+ "repoUrl": "https://github.com/fedimint/fedimint",
+ "containerConfig": {
+ "ports": [
+ "8176:8176",
+ "9737:9737"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/fedimint-gateway:/data",
+ "/var/lib/archipelago/lnd:/lnd:ro"
+ ]
+ }
+ },
{
"id": "jellyfin",
"title": "Jellyfin",
@@ -226,8 +348,13 @@
"dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13",
"repoUrl": "https://github.com/jellyfin/jellyfin",
"containerConfig": {
- "ports": ["8096:8096"],
- "volumes": ["/var/lib/archipelago/jellyfin/config:/config", "/var/lib/archipelago/jellyfin/cache:/cache"]
+ "ports": [
+ "8096:8096"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/jellyfin/config:/config",
+ "/var/lib/archipelago/jellyfin/cache:/cache"
+ ]
}
},
{
@@ -244,34 +371,47 @@
{
"id": "homeassistant",
"title": "Home Assistant",
- "version": "2024.1",
- "description": "Open-source home automation.",
+ "version": "2024.1.0",
+ "description": "Open source home automation platform. Control and monitor your smart home devices.",
"icon": "/assets/img/app-icons/homeassistant.png",
"author": "Home Assistant",
"category": "home",
"dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1",
"repoUrl": "https://github.com/home-assistant/core",
"containerConfig": {
- "ports": ["8123:8123"],
- "volumes": ["/var/lib/archipelago/home-assistant:/config"],
- "env": ["TZ=UTC"]
+ "ports": [
+ "8123:8123"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/home-assistant:/config"
+ ],
+ "env": [
+ "TZ=UTC"
+ ]
}
},
{
"id": "grafana",
"title": "Grafana",
"version": "10.2.0",
- "description": "Analytics and monitoring dashboards.",
+ "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
"icon": "/assets/img/app-icons/grafana.png",
"author": "Grafana Labs",
"category": "data",
"tier": "recommended",
- "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0",
+ "dockerImage": "grafana/grafana:10.2.0",
"repoUrl": "https://github.com/grafana/grafana",
"containerConfig": {
- "ports": ["3000:3000"],
- "volumes": ["/var/lib/archipelago/grafana:/var/lib/grafana"],
- "env": ["GF_PATHS_DATA=/var/lib/grafana", "GF_USERS_ALLOW_SIGN_UP=false"]
+ "ports": [
+ "3000:3000"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/grafana:/var/lib/grafana"
+ ],
+ "env": [
+ "GF_PATHS_DATA=/var/lib/grafana",
+ "GF_USERS_ALLOW_SIGN_UP=false"
+ ]
}
},
{
@@ -286,10 +426,42 @@
"dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable",
"repoUrl": "https://github.com/tailscale/tailscale",
"containerConfig": {
- "ports": ["8240:8240"],
- "volumes": ["/var/lib/archipelago/tailscale:/var/lib/tailscale"],
- "env": ["TS_STATE_DIR=/var/lib/tailscale"],
- "args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"]
+ "ports": [
+ "8240:8240"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/tailscale:/var/lib/tailscale"
+ ],
+ "env": [
+ "TS_STATE_DIR=/var/lib/tailscale"
+ ],
+ "args": [
+ "sh",
+ "-c",
+ "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait"
+ ]
+ }
+ },
+ {
+ "id": "portainer",
+ "title": "Portainer",
+ "version": "2.19.4",
+ "description": "Container management web UI for the local Podman socket.",
+ "icon": "/assets/img/app-icons/portainer.webp",
+ "author": "Portainer",
+ "category": "development",
+ "tier": "optional",
+ "dockerImage": "146.59.87.168:3000/lfg2025/portainer:latest",
+ "repoUrl": "https://github.com/portainer/portainer",
+ "containerConfig": {
+ "ports": [
+ "9000:9000"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/portainer:/data",
+ "/run/user/1000/podman/podman.sock:/var/run/docker.sock"
+ ],
+ "notes": "Uses the manifest-owned Podman socket bind mount preparation path."
}
},
{
@@ -304,8 +476,14 @@
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
"repoUrl": "https://github.com/netbirdio/netbird",
"containerConfig": {
- "ports": ["8087:80", "8086:80", "3478:3478/udp"],
- "volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
+ "ports": [
+ "8087:80",
+ "8086:80",
+ "3478:3478/udp"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/netbird:/var/lib/netbird"
+ ],
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
}
},
@@ -321,10 +499,20 @@
"dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1",
"repoUrl": "https://github.com/louislam/uptime-kuma",
"containerConfig": {
- "ports": ["3002:3001"],
- "volumes": ["/var/lib/archipelago/uptime-kuma:/app/data"],
- "env": ["TZ=UTC"],
- "args": ["--", "node", "server/server.js"]
+ "ports": [
+ "3002:3001"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/uptime-kuma:/app/data"
+ ],
+ "env": [
+ "TZ=UTC"
+ ],
+ "args": [
+ "--",
+ "node",
+ "server/server.js"
+ ]
}
},
{
@@ -338,24 +526,35 @@
"dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915",
"repoUrl": "https://github.com/photoprism/photoprism",
"containerConfig": {
- "ports": ["2342:2342"],
- "volumes": ["/var/lib/archipelago/photoprism:/photoprism/storage"],
- "env": ["PHOTOPRISM_ADMIN_PASSWORD=archipelago", "PHOTOPRISM_DEFAULT_LOCALE=en"]
+ "ports": [
+ "2342:2342"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/photoprism:/photoprism/storage"
+ ],
+ "env": [
+ "PHOTOPRISM_ADMIN_PASSWORD=archipelago",
+ "PHOTOPRISM_DEFAULT_LOCALE=en"
+ ]
}
},
{
"id": "nextcloud",
"title": "Nextcloud",
- "version": "28",
+ "version": "29",
"description": "Your own private cloud. File sync, calendars, contacts.",
"icon": "/assets/img/app-icons/nextcloud.webp",
"author": "Nextcloud",
"category": "data",
- "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28",
+ "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:29",
"repoUrl": "https://github.com/nextcloud/server",
"containerConfig": {
- "ports": ["8085:80"],
- "volumes": ["/var/lib/archipelago/nextcloud:/var/www/html"]
+ "ports": [
+ "8085:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/nextcloud:/var/www/html"
+ ]
}
}
]
diff --git a/apps/archy-nbxplorer/manifest.yml b/apps/archy-nbxplorer/manifest.yml
index 1d48d74c..f4b7cd99 100644
--- a/apps/archy-nbxplorer/manifest.yml
+++ b/apps/archy-nbxplorer/manifest.yml
@@ -56,8 +56,8 @@ app:
endpoint: http://localhost:32838
path: /
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
bitcoin_integration:
rpc_access: read-only
diff --git a/apps/bitcoin-knots/manifest.yml b/apps/bitcoin-knots/manifest.yml
index 0e390f95..23001742 100644
--- a/apps/bitcoin-knots/manifest.yml
+++ b/apps/bitcoin-knots/manifest.yml
@@ -28,11 +28,17 @@ app:
fi;
RPC_USER="$(printenv BITCOIN_RPC_USER)";
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
+ RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
DISK_GB_VALUE="$(printenv DISK_GB || true)";
+ RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
+ RPC_TXRELAY_FLAGS="-rpcwhitelistdefault=0";
+ if [ -n "$RPC_TXRELAY_AUTH" ]; then
+ RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblockheader,getrawtransaction,decoderawtransaction,decodescript,estimatesmartfee";
+ fi;
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
- exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
+ exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
else
- exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
+ exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
fi
derived_env:
- key: DISK_GB
@@ -40,6 +46,8 @@ app:
secret_env:
- key: BITCOIN_RPC_PASS
secret_file: bitcoin-rpc-password
+ - key: BITCOIN_RPC_TXRELAY_RPCAUTH
+ secret_file: bitcoin-rpc-txrelay-rpcauth
data_uid: "100101:100101"
dependencies:
diff --git a/apps/botfights/manifest.yml b/apps/botfights/manifest.yml
index 736a927c..3fcb25cf 100644
--- a/apps/botfights/manifest.yml
+++ b/apps/botfights/manifest.yml
@@ -1,12 +1,12 @@
app:
id: botfights
name: BotFights
- version: 1.0.0
+ version: 1.1.0
description: Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.
category: community
container:
- image: git.tx1138.com/lfg2025/botfights:1.1.0
+ image: 146.59.87.168:3000/lfg2025/botfights:1.1.0
pull_policy: always
dependencies:
@@ -62,6 +62,8 @@ app:
metadata:
author: Dorian
+ repo: https://botfights.net
+ icon: /assets/img/app-icons/botfights.svg
license: MIT
tags:
- bitcoin
diff --git a/apps/btcpay-server/manifest.yml b/apps/btcpay-server/manifest.yml
index 6c42c267..4f0d9af0 100644
--- a/apps/btcpay-server/manifest.yml
+++ b/apps/btcpay-server/manifest.yml
@@ -60,8 +60,8 @@ app:
endpoint: http://localhost:49392
path: /
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
bitcoin_integration:
rpc_access: read-only
@@ -79,3 +79,7 @@ app:
port: 23000
protocol: http
path: /
+
+ metadata:
+ launch:
+ open_in_new_tab: true
diff --git a/apps/electrumx/manifest.yml b/apps/electrumx/manifest.yml
index 395f1069..e765754a 100644
--- a/apps/electrumx/manifest.yml
+++ b/apps/electrumx/manifest.yml
@@ -5,7 +5,7 @@ app:
description: Electrum server indexing Bitcoin chain data for lightweight wallet queries.
container:
- image: git.tx1138.com/lfg2025/electrumx:v1.18.0
+ image: 146.59.87.168:3000/lfg2025/electrumx:v1.18.0
pull_policy: if-not-present
network: archy-net
data_uid: "1000:1000"
diff --git a/apps/fedimint/manifest.yml b/apps/fedimint/manifest.yml
index ca6f4154..88094578 100644
--- a/apps/fedimint/manifest.yml
+++ b/apps/fedimint/manifest.yml
@@ -5,9 +5,17 @@ app:
description: Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.
container:
- image: git.tx1138.com/lfg2025/fedimintd:v0.10.0
+ image: 146.59.87.168:3000/lfg2025/fedimintd:v0.10.0
pull_policy: if-not-present
network: archy-net
+ entrypoint: ["sh", "-lc"]
+ custom_args:
+ - |-
+ until state="$(curl -sS --connect-timeout 5 -m 45 -u "$FM_BITCOIND_USERNAME:$FM_BITCOIND_PASSWORD" -H "Content-Type: application/json" --data-binary '{"jsonrpc":"1.0","id":"fedimint-wait","method":"getblockchaininfo","params":[]}' "$FM_BITCOIND_URL/")" && echo "$state" | grep -q '"initialblockdownload":false'; do
+ echo "Waiting for Bitcoin RPC sync at $FM_BITCOIND_URL...";
+ sleep 30;
+ done;
+ exec fedimintd
derived_env:
- key: FM_P2P_URL
template: fedimint://{{HOST_MDNS}}:8173
@@ -40,7 +48,9 @@ app:
- host: 8174
container: 8174
protocol: tcp
- - host: 8175
+ # Public launch port 8175 is owned by archy-fedimint-ui, which serves a
+ # wait page while Bitcoin syncs and proxies here after fedimintd starts.
+ - host: 8177
container: 8175
protocol: tcp
@@ -52,7 +62,7 @@ app:
environment:
- FM_DATA_DIR=/data
- - FM_BITCOIND_URL=http://host.archipelago:8332
+ - FM_BITCOIND_URL=http://bitcoin-knots:8332
- FM_BITCOIND_USERNAME=archipelago
- FM_BITCOIN_NETWORK=bitcoin
- FM_BIND_P2P=0.0.0.0:8173
@@ -67,6 +77,15 @@ app:
timeout: 5s
retries: 3
+ interfaces:
+ main:
+ name: Guardian UI
+ description: Fedimint Guardian wait/proxy UI
+ type: ui
+ port: 8175
+ protocol: http
+ path: /
+
bitcoin_integration:
rpc_access: admin
sync_required: true
diff --git a/apps/gitea/manifest.yml b/apps/gitea/manifest.yml
index 9f0053cf..3f80b57d 100644
--- a/apps/gitea/manifest.yml
+++ b/apps/gitea/manifest.yml
@@ -1,52 +1,87 @@
-id: gitea
-name: Gitea
-version: "1.23"
-description: Self-hosted Git service with built-in container registry, CI/CD, and package hosting.
-category: development
-icon: git-branch
-port: 3000
-internal_port: 3001
-ssh_port: 2222
-image: docker.io/gitea/gitea:1.23
-tier: optional
+app:
+ id: gitea
+ name: Gitea
+ version: "1.23"
+ description: Self-hosted Git service with built-in container registry, CI/CD, and package hosting.
+ category: development
-requires:
- memory_mb: 256
- disk_mb: 500
+ container:
+ image: docker.io/gitea/gitea:1.23
+ pull_policy: if-not-present
-volumes:
- - host: /var/lib/archipelago/gitea/data
- container: /data
- - host: /var/lib/archipelago/gitea/config
- container: /etc/gitea
+ dependencies:
+ - storage: 500Mi
-environment:
- GITEA__database__DB_TYPE: sqlite3
- GITEA__server__SSH_PORT: "2222"
- GITEA__server__SSH_LISTEN_PORT: "22"
- GITEA__server__LFS_START_SERVER: "true"
- GITEA__packages__ENABLED: "true"
- GITEA__repository__ENABLE_PUSH_CREATE_USER: "true"
- GITEA__repository__ENABLE_PUSH_CREATE_ORG: "true"
+ resources:
+ memory_limit: 256Mi
+ disk_limit: 500Mi
-# Gitea hardcodes X-Frame-Options: SAMEORIGIN, so Archipelago opens it in a
-# new tab on host port 3001 instead of embedding it in an iframe.
-nginx_proxy:
- listen: 3000
- proxy_pass: "http://127.0.0.1:3001"
- extra_headers:
- - "proxy_hide_header X-Frame-Options"
- - "proxy_hide_header Content-Security-Policy"
+ security:
+ capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE]
+ readonly_root: false
+ no_new_privileges: false
+ network_policy: bridge
-health_check:
- endpoint: /
- interval: 120
- timeout: 5
- retries: 3
+ ports:
+ - host: 3001
+ container: 3000
+ protocol: tcp
+ - host: 2222
+ container: 22
+ protocol: tcp
-features:
- - Git repositories with web UI
- - Built-in container/package registry
- - Issue tracking and pull requests
- - CI/CD via Gitea Actions
- - Lightweight (SQLite, no external DB needed)
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/gitea/data
+ target: /data
+ options: [rw]
+ - type: bind
+ source: /var/lib/archipelago/gitea/config
+ target: /etc/gitea
+ options: [rw]
+
+ environment:
+ - GITEA__database__DB_TYPE=sqlite3
+ - GITEA__server__SSH_PORT=2222
+ - GITEA__server__SSH_LISTEN_PORT=22
+ - GITEA__server__LFS_START_SERVER=true
+ - GITEA__packages__ENABLED=true
+ - GITEA__repository__ENABLE_PUSH_CREATE_USER=true
+ - GITEA__repository__ENABLE_PUSH_CREATE_ORG=true
+
+ health_check:
+ type: http
+ endpoint: http://localhost:3000
+ path: /
+ interval: 120s
+ timeout: 30s
+ retries: 5
+
+ interfaces:
+ main:
+ name: Web UI
+ description: Gitea web interface
+ type: ui
+ port: 3001
+ protocol: http
+ path: /
+
+ metadata:
+ icon: /assets/img/app-icons/gitea.svg
+ repo: https://gitea.com
+ tier: optional
+ launch:
+ open_in_new_tab: true
+ features:
+ - Git repositories with web UI
+ - Built-in container/package registry
+ - Issue tracking and pull requests
+ - CI/CD via Gitea Actions
+ - Lightweight SQLite deployment
+
+ nginx_proxy:
+ listen: 3000
+ proxy_pass: http://127.0.0.1:3001
+ extra_headers:
+ - proxy_hide_header X-Frame-Options
+ - proxy_hide_header Content-Security-Policy
diff --git a/apps/grafana/manifest.yml b/apps/grafana/manifest.yml
index 85fe534a..ed6a6b42 100644
--- a/apps/grafana/manifest.yml
+++ b/apps/grafana/manifest.yml
@@ -49,5 +49,9 @@ app:
endpoint: http://localhost:3000
path: /api/health
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
+
+ metadata:
+ launch:
+ open_in_new_tab: true
diff --git a/apps/home-assistant/manifest.yml b/apps/home-assistant/manifest.yml
index fb502f63..4c76ba44 100644
--- a/apps/home-assistant/manifest.yml
+++ b/apps/home-assistant/manifest.yml
@@ -1,29 +1,29 @@
app:
- id: home-assistant
+ id: homeassistant
name: Home Assistant
version: 2024.1.0
description: Open source home automation platform. Control and monitor your smart home devices.
container:
- image: homeassistant/home-assistant:2024.1
- image_signature: cosign://...
+ image: 146.59.87.168:3000/lfg2025/home-assistant:2024.1
pull_policy: if-not-present
+ network: pasta
dependencies:
- storage: 10Gi
resources:
cpu_limit: 2
- memory_limit: 2Gi
+ memory_limit: 512Mi
disk_limit: 10Gi
security:
- capabilities: [NET_BIND_SERVICE]
+ capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE, NET_RAW]
readonly_root: false # Home Assistant needs write access
no_new_privileges: true
user: 1000
seccomp_profile: default
- network_policy: host # Requires host network for device discovery
+ network_policy: isolated
apparmor_profile: home-assistant
ports:
@@ -36,24 +36,23 @@ app:
source: /var/lib/archipelago/home-assistant
target: /config
options: [rw]
- - type: bind
- source: /var/run/dbus
- target: /var/run/dbus
- options: [ro]
- devices:
- - /dev/ttyUSB0 # Serial devices
- - /dev/ttyACM0 # USB devices
+ devices: []
environment:
- TZ=UTC
- - PUID=1000
- - PGID=1000
health_check:
- type: http
- endpoint: http://localhost:8123
- path: /
+ type: tcp
+ endpoint: localhost:8123
interval: 30s
timeout: 5s
retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/homeassistant.png
+ category: home
+ author: Home Assistant
+ repo: https://github.com/home-assistant/core
+ launch:
+ open_in_new_tab: true
diff --git a/apps/indeedhub/manifest.yml b/apps/indeedhub/manifest.yml
index 0e443741..cf625470 100644
--- a/apps/indeedhub/manifest.yml
+++ b/apps/indeedhub/manifest.yml
@@ -1,12 +1,12 @@
app:
id: indeedhub
- name: Indeehub
- version: 0.1.0
+ name: IndeeHub
+ version: 1.0.0
description: Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.
- category: media
+ category: community
container:
- image: 146.59.87.168:3000/lfg2025/indeedhub:latest
+ image: 146.59.87.168:3000/lfg2025/indeedhub:1.0.0
pull_policy: always # Pull from registry; falls back to local build
network: indeedhub-net
@@ -70,8 +70,9 @@ app:
metadata:
author: Indeehub Team
+ icon: /assets/img/app-icons/indeedhub.png
website: https://indeedhub.com
- source: https://github.com/indeedhub/indeedhub
+ repo: https://github.com/indeedhub/indeedhub
license: MIT
tags:
- bitcoin
diff --git a/apps/jellyfin/manifest.yml b/apps/jellyfin/manifest.yml
new file mode 100644
index 00000000..410d61ca
--- /dev/null
+++ b/apps/jellyfin/manifest.yml
@@ -0,0 +1,52 @@
+app:
+ id: jellyfin
+ name: Jellyfin
+ version: 10.8.13
+ description: Free media server. Stream movies, music, and photos.
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/jellyfin:10.8.13
+ pull_policy: if-not-present
+ network: pasta
+
+ dependencies:
+ - storage: 10Gi
+
+ resources:
+ memory_limit: 1Gi
+ disk_limit: 10Gi
+
+ security:
+ capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE]
+ readonly_root: false
+ network_policy: isolated
+
+ ports:
+ - host: 8096
+ container: 8096
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/jellyfin/config
+ target: /config
+ options: [rw]
+ - type: bind
+ source: /var/lib/archipelago/jellyfin/cache
+ target: /cache
+ options: [rw]
+
+ environment: []
+
+ health_check:
+ type: tcp
+ endpoint: localhost:8096
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/jellyfin.webp
+ category: data
+ author: Jellyfin
+ repo: https://github.com/jellyfin/jellyfin
diff --git a/apps/lnd/manifest.yml b/apps/lnd/manifest.yml
index 0e39aff5..ec5e2ee4 100644
--- a/apps/lnd/manifest.yml
+++ b/apps/lnd/manifest.yml
@@ -1,11 +1,11 @@
app:
id: lnd
- name: Lightning Network Daemon
+ name: LND
version: 0.18.4
description: Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.
container:
- image: git.tx1138.com/lfg2025/lnd:v0.18.4-beta
+ image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta
pull_policy: if-not-present
network: archy-net
secret_env:
diff --git a/apps/mempool/manifest.yml b/apps/mempool/manifest.yml
index bbce179a..b16dff95 100644
--- a/apps/mempool/manifest.yml
+++ b/apps/mempool/manifest.yml
@@ -1,11 +1,11 @@
app:
id: mempool
- name: Mempool
- version: 2.5.0
+ name: Mempool Explorer
+ version: 3.0.0
description: Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.
container:
- image: mempool/mempool:v2.5.0
+ image: 146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0
image_signature: cosign://...
pull_policy: if-not-present
diff --git a/apps/meshtastic/manifest.yml b/apps/meshtastic/manifest.yml
index dd6698b8..d8aecdd4 100644
--- a/apps/meshtastic/manifest.yml
+++ b/apps/meshtastic/manifest.yml
@@ -1,13 +1,12 @@
app:
id: meshtastic
name: Meshtastic
- version: 2.5.0
+ version: 2-daily-alpine
description: Open-source mesh networking for LoRa radios. Create decentralized communication networks.
container:
- image: meshtastic/meshtasticd:2.5.6
- image_signature: cosign://...
- pull_policy: verify-signature
+ image: docker.io/meshtastic/meshtasticd:daily-alpine
+ pull_policy: if-not-present
dependencies:
- storage: 1Gi
@@ -29,33 +28,42 @@ app:
ports:
- host: 4403
container: 4403
- protocol: tcp # HTTP API
- - host: 1883
- container: 1883
- protocol: tcp # MQTT (optional)
+ protocol: tcp # Meshtastic TCP API
devices:
- /dev/ttyUSB0 # LoRa radio device (if connected)
- - /dev/ttyACM0 # Alternative device path
volumes:
- type: bind
source: /var/lib/archipelago/meshtastic
- target: /app/data
+ target: /var/lib/meshtasticd
options: [rw]
+
+ files:
+ - path: /var/lib/archipelago/meshtastic/config.yaml
+ content: |
+ General:
+ MACAddress: AA:BB:CC:DD:EE:01
+ Webserver:
+ Port: 4403
environment:
- MESHTASTIC_PORT=/dev/ttyUSB0
- MESHTASTIC_SERIAL=true
health_check:
- type: http
- endpoint: http://localhost:4403
- path: /health
+ type: cmd
+ endpoint: test -f /var/lib/meshtasticd/config.yaml
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
networking:
mesh_enabled: true
local_network_access: true
+
+ metadata:
+ icon: /assets/img/app-icons/meshcore.svg
+ category: networking
+ tier: recommended
+ repo: https://github.com/meshtastic/firmware
diff --git a/apps/nextcloud/manifest.yml b/apps/nextcloud/manifest.yml
new file mode 100644
index 00000000..d438e583
--- /dev/null
+++ b/apps/nextcloud/manifest.yml
@@ -0,0 +1,50 @@
+app:
+ id: nextcloud
+ name: Nextcloud
+ version: "29"
+ description: Your own private cloud. File sync, calendars, contacts.
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/nextcloud:29
+ pull_policy: if-not-present
+ network: pasta
+
+ dependencies:
+ - storage: 10Gi
+
+ resources:
+ memory_limit: 1Gi
+ disk_limit: 10Gi
+
+ security:
+ capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE]
+ readonly_root: false
+ network_policy: isolated
+
+ ports:
+ - host: 8085
+ container: 80
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/nextcloud
+ target: /var/www/html
+ options: [rw]
+
+ environment: []
+
+ health_check:
+ type: tcp
+ endpoint: localhost:80
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/nextcloud.webp
+ category: data
+ author: Nextcloud
+ repo: https://github.com/nextcloud/server
+ launch:
+ open_in_new_tab: true
diff --git a/apps/nostr-rs-relay/manifest.yml b/apps/nostr-rs-relay/manifest.yml
index 073005bc..975a07ae 100644
--- a/apps/nostr-rs-relay/manifest.yml
+++ b/apps/nostr-rs-relay/manifest.yml
@@ -28,7 +28,7 @@ app:
apparmor_profile: nostr-relay
ports:
- - host: 8081
+ - host: 18081
container: 8080
protocol: tcp # HTTP/WebSocket
@@ -49,8 +49,8 @@ app:
endpoint: http://localhost:8080
path: /
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
nostr_integration:
relay_type: public
diff --git a/apps/onlyoffice/manifest.yml b/apps/onlyoffice/manifest.yml
index 16285d7f..30881cba 100644
--- a/apps/onlyoffice/manifest.yml
+++ b/apps/onlyoffice/manifest.yml
@@ -48,3 +48,7 @@ app:
interval: 30s
timeout: 5s
retries: 3
+
+ metadata:
+ launch:
+ open_in_new_tab: true
diff --git a/apps/photoprism/manifest.yml b/apps/photoprism/manifest.yml
new file mode 100644
index 00000000..2966300a
--- /dev/null
+++ b/apps/photoprism/manifest.yml
@@ -0,0 +1,51 @@
+app:
+ id: photoprism
+ name: PhotoPrism
+ version: "240915"
+ description: AI-powered photo management with facial recognition.
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/photoprism:240915
+ pull_policy: if-not-present
+
+ dependencies:
+ - storage: 10Gi
+
+ resources:
+ memory_limit: 1Gi
+ disk_limit: 10Gi
+
+ security:
+ capabilities: [CHOWN, SETUID, SETGID]
+ readonly_root: false
+ network_policy: isolated
+
+ ports:
+ - host: 2342
+ container: 2342
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/photoprism
+ target: /photoprism/storage
+ options: [rw]
+
+ environment:
+ - PHOTOPRISM_ADMIN_PASSWORD=archipelago
+ - PHOTOPRISM_DEFAULT_LOCALE=en
+
+ health_check:
+ type: tcp
+ endpoint: localhost:2342
+ interval: 60s
+ timeout: 5s
+ retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/photoprism.svg
+ category: data
+ author: PhotoPrism
+ repo: https://github.com/photoprism/photoprism
+ launch:
+ open_in_new_tab: true
diff --git a/apps/portainer/manifest.yml b/apps/portainer/manifest.yml
new file mode 100644
index 00000000..96e63f1e
--- /dev/null
+++ b/apps/portainer/manifest.yml
@@ -0,0 +1,64 @@
+app:
+ id: portainer
+ name: Portainer
+ version: 2.19.4
+ description: Container management web UI for the local Podman socket.
+ category: development
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/portainer:latest
+ pull_policy: if-not-present
+ data_uid: "1000:1000"
+
+ dependencies:
+ - storage: 1Gi
+
+ resources:
+ memory_limit: 256Mi
+ disk_limit: 1Gi
+
+ security:
+ capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE]
+ readonly_root: false
+ no_new_privileges: true
+ network_policy: isolated
+
+ ports:
+ - host: 9000
+ container: 9000
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/portainer
+ target: /data
+ options: [rw]
+ - type: bind
+ source: /var/lib/archipelago/portainer/compose
+ target: /data/compose
+ options: [rw]
+ - type: bind
+ source: /run/user/1000/podman/podman.sock
+ target: /var/run/docker.sock
+ options: [rw]
+
+ environment: []
+
+ interfaces:
+ main:
+ name: Web UI
+ description: Portainer web interface
+ type: ui
+ port: 9000
+ protocol: http
+ path: /
+
+ metadata:
+ icon: /assets/img/app-icons/portainer.webp
+ tier: optional
+ launch:
+ open_in_new_tab: true
+ features:
+ - Container management dashboard
+ - Local Podman socket access
+ - Compose stack storage
diff --git a/apps/searxng/manifest.yml b/apps/searxng/manifest.yml
index db8cb286..1eeb727d 100644
--- a/apps/searxng/manifest.yml
+++ b/apps/searxng/manifest.yml
@@ -45,5 +45,5 @@ app:
endpoint: http://localhost:8080
path: /
interval: 30s
- timeout: 5s
- retries: 3
+ timeout: 30s
+ retries: 5
diff --git a/apps/uptime-kuma/manifest.yml b/apps/uptime-kuma/manifest.yml
new file mode 100644
index 00000000..e58f0bbe
--- /dev/null
+++ b/apps/uptime-kuma/manifest.yml
@@ -0,0 +1,54 @@
+app:
+ id: uptime-kuma
+ name: Uptime Kuma
+ version: 1.23.0
+ description: Self-hosted uptime monitoring.
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/uptime-kuma:1
+ pull_policy: if-not-present
+ network: pasta
+ custom_args: ["--", "node", "server/server.js"]
+
+ dependencies:
+ - storage: 1Gi
+
+ resources:
+ memory_limit: 256Mi
+ disk_limit: 1Gi
+
+ security:
+ capabilities: [CHOWN, FOWNER, SETUID, SETGID]
+ readonly_root: false
+ network_policy: isolated
+
+ ports:
+ - host: 3002
+ container: 3001
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/uptime-kuma
+ target: /app/data
+ options: [rw]
+
+ environment:
+ - TZ=UTC
+
+ health_check:
+ type: http
+ endpoint: localhost:3001
+ path: /
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/uptime-kuma.webp
+ category: data
+ tier: recommended
+ author: Uptime Kuma
+ repo: https://github.com/louislam/uptime-kuma
+ launch:
+ open_in_new_tab: true
diff --git a/apps/vaultwarden/manifest.yml b/apps/vaultwarden/manifest.yml
new file mode 100644
index 00000000..c7718267
--- /dev/null
+++ b/apps/vaultwarden/manifest.yml
@@ -0,0 +1,51 @@
+app:
+ id: vaultwarden
+ name: Vaultwarden
+ version: 1.30.0
+ description: Self-hosted password vault with zero-knowledge encryption.
+
+ container:
+ image: 146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine
+ pull_policy: if-not-present
+ network: pasta
+
+ dependencies:
+ - storage: 1Gi
+
+ resources:
+ memory_limit: 256Mi
+ disk_limit: 1Gi
+
+ security:
+ capabilities: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE]
+ readonly_root: false
+ network_policy: isolated
+
+ ports:
+ - host: 8082
+ container: 80
+ protocol: tcp
+
+ volumes:
+ - type: bind
+ source: /var/lib/archipelago/vaultwarden
+ target: /data
+ options: [rw]
+
+ environment: []
+
+ health_check:
+ type: tcp
+ endpoint: localhost:80
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ metadata:
+ icon: /assets/img/app-icons/vaultwarden.webp
+ category: data
+ tier: recommended
+ author: Vaultwarden
+ repo: https://github.com/dani-garcia/vaultwarden
+ launch:
+ open_in_new_tab: true
diff --git a/neode-ui/public/assets/img/app-icons/archipelago-a.svg b/neode-ui/public/assets/img/app-icons/archipelago-a.svg
new file mode 100644
index 00000000..7989db9c
--- /dev/null
+++ b/neode-ui/public/assets/img/app-icons/archipelago-a.svg
@@ -0,0 +1,3 @@
+
diff --git a/neode-ui/public/assets/img/app-icons/meshcore.svg b/neode-ui/public/assets/img/app-icons/meshcore.svg
new file mode 100644
index 00000000..30469c96
--- /dev/null
+++ b/neode-ui/public/assets/img/app-icons/meshcore.svg
@@ -0,0 +1,28 @@
+
diff --git a/neode-ui/public/assets/img/app-icons/saleor.svg b/neode-ui/public/assets/img/app-icons/saleor.svg
deleted file mode 100644
index 0720b6e1..00000000
--- a/neode-ui/public/assets/img/app-icons/saleor.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json
index 68e55d82..e55d4b0d 100644
--- a/neode-ui/public/catalog.json
+++ b/neode-ui/public/catalog.json
@@ -14,7 +14,7 @@
"id": "bitcoin-knots",
"title": "Bitcoin Knots",
"version": "28.1.0",
- "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.",
+ "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
"author": "Bitcoin Knots",
"category": "money",
@@ -25,8 +25,8 @@
{
"id": "bitcoin-core",
"title": "Bitcoin Core",
- "version": "28.4",
- "description": "Reference Bitcoin node implementation. Alternative to Bitcoin Knots; uninstall Knots before switching.",
+ "version": "28.4.0",
+ "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.",
"icon": "/assets/img/app-icons/bitcoin-core.svg",
"author": "Bitcoin Core contributors",
"category": "money",
@@ -38,7 +38,7 @@
"id": "lnd",
"title": "LND",
"version": "0.18.4",
- "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.",
+ "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.",
"icon": "/assets/img/app-icons/lnd.svg",
"author": "Lightning Labs",
"category": "money",
@@ -53,7 +53,7 @@
"id": "btcpay-server",
"title": "BTCPay Server",
"version": "2.3.9",
- "description": "Self-hosted Bitcoin payment processor.",
+ "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.",
"icon": "/assets/img/app-icons/btcpay-server.png",
"author": "BTCPay Server Foundation",
"category": "commerce",
@@ -76,8 +76,17 @@
"dockerImage": "ghcr.io/saleor/saleor:3.23",
"repoUrl": "https://github.com/saleor/saleor",
"containerConfig": {
- "ports": ["9011:80", "9010:80", "8000:8000", "8025:8025", "16686:16686"],
- "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"],
+ "ports": [
+ "9011:80",
+ "9010:80",
+ "8000:8000",
+ "8025:8025",
+ "16686:16686"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/saleor:/app/media",
+ "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"
+ ],
"notes": "Installed as a Saleor stack: customer storefront on 9011, admin dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor."
}
},
@@ -85,7 +94,7 @@
"id": "mempool",
"title": "Mempool Explorer",
"version": "3.0.0",
- "description": "Self-hosted Bitcoin blockchain and mempool visualizer.",
+ "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.",
"icon": "/assets/img/app-icons/mempool.webp",
"author": "Mempool",
"category": "money",
@@ -101,7 +110,7 @@
"id": "electrumx",
"title": "ElectrumX",
"version": "1.18.0",
- "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.",
+ "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.",
"icon": "/assets/img/app-icons/electrumx.png",
"author": "Luke Childs",
"category": "money",
@@ -116,7 +125,7 @@
"id": "indeedhub",
"title": "IndeeHub",
"version": "1.0.0",
- "description": "Bitcoin documentary streaming with Nostr identity.",
+ "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.",
"icon": "/assets/img/app-icons/indeedhub.png",
"author": "IndeeHub",
"category": "community",
@@ -127,49 +136,133 @@
"id": "botfights",
"title": "BotFights",
"version": "1.1.0",
- "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.",
+ "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.",
"icon": "/assets/img/app-icons/botfights.svg",
"author": "BotFights",
"category": "community",
"dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0",
"repoUrl": "https://botfights.net",
"containerConfig": {
- "ports": ["9100:9100"],
- "volumes": ["/var/lib/archipelago/botfights:/app/server/data"],
- "env": ["NODE_ENV=production", "PORT=9100", "FIGHT_LOOP_ENABLED=true", "ARCHY_EMBEDDED=1"]
+ "ports": [
+ "9100:9100"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/botfights:/app/server/data"
+ ],
+ "env": [
+ "NODE_ENV=production",
+ "PORT=9100",
+ "FIGHT_LOOP_ENABLED=true",
+ "ARCHY_EMBEDDED=1"
+ ]
}
},
{
"id": "gitea",
"title": "Gitea",
"version": "1.23",
- "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.",
+ "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.",
"icon": "/assets/img/app-icons/gitea.svg",
"author": "Gitea",
"category": "development",
- "dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23",
+ "dockerImage": "docker.io/gitea/gitea:1.23",
"repoUrl": "https://gitea.com",
"containerConfig": {
- "ports": ["3001:3000", "2222:22"],
- "volumes": ["/var/lib/archipelago/gitea/data:/data", "/var/lib/archipelago/gitea/config:/etc/gitea"],
- "env": ["GITEA__database__DB_TYPE=sqlite3", "GITEA__server__SSH_PORT=2222", "GITEA__server__SSH_LISTEN_PORT=22", "GITEA__server__LFS_START_SERVER=true", "GITEA__packages__ENABLED=true", "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", "GITEA__security__X_FRAME_OPTIONS="]
- }
+ "ports": [
+ "3001:3000",
+ "2222:22"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/gitea/data:/data",
+ "/var/lib/archipelago/gitea/config:/etc/gitea"
+ ],
+ "env": [
+ "GITEA__database__DB_TYPE=sqlite3",
+ "GITEA__server__SSH_PORT=2222",
+ "GITEA__server__SSH_LISTEN_PORT=22",
+ "GITEA__server__LFS_START_SERVER=true",
+ "GITEA__packages__ENABLED=true",
+ "GITEA__repository__ENABLE_PUSH_CREATE_USER=true",
+ "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true",
+ "GITEA__security__X_FRAME_OPTIONS="
+ ]
+ },
+ "tier": "optional"
},
{
"id": "filebrowser",
"title": "File Browser",
"version": "2.27.0",
- "description": "Web-based file manager.",
+ "description": "Baseline Archipelago file manager service.",
"icon": "/assets/img/app-icons/file-browser.webp",
"author": "File Browser",
"category": "data",
"tier": "core",
- "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0",
+ "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0",
"repoUrl": "https://github.com/filebrowser/filebrowser",
"containerConfig": {
- "ports": ["8083:80"],
- "volumes": ["/var/lib/archipelago/filebrowser:/srv", "/var/lib/archipelago/filebrowser-data:/data"],
- "args": ["--database=/data/database.db", "--root=/srv", "--address=0.0.0.0", "--port=80"]
+ "ports": [
+ "8083:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/filebrowser:/srv",
+ "/var/lib/archipelago/filebrowser-data:/data"
+ ],
+ "args": [
+ "--database=/data/database.db",
+ "--root=/srv",
+ "--address=0.0.0.0",
+ "--port=80"
+ ]
+ }
+ },
+ {
+ "id": "nostr-rs-relay",
+ "title": "Nostr Relay (Rust)",
+ "version": "0.8.0",
+ "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.",
+ "icon": "/assets/img/app-icons/nostr.svg",
+ "author": "Nostr RS Relay",
+ "category": "community",
+ "tier": "recommended",
+ "dockerImage": "scsibug/nostr-rs-relay:0.8.9",
+ "repoUrl": "https://github.com/scsibug/nostr-rs-relay",
+ "containerConfig": {
+ "ports": [
+ "8081:8080"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/nostr-relay:/usr/src/app/db"
+ ],
+ "env": [
+ "RELAY_NAME=Archipelago Nostr Relay",
+ "RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago"
+ ]
+ }
+ },
+ {
+ "id": "meshtastic",
+ "title": "Meshtastic",
+ "version": "2-daily-alpine",
+ "description": "Open-source mesh networking for LoRa radios. Create decentralized communication networks.",
+ "icon": "/assets/img/app-icons/meshcore.svg",
+ "author": "Meshtastic",
+ "category": "networking",
+ "tier": "recommended",
+ "dockerImage": "docker.io/meshtastic/meshtasticd:daily-alpine",
+ "repoUrl": "https://github.com/meshtastic/firmware",
+ "containerConfig": {
+ "ports": [
+ "4403:4403"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/meshtastic:/var/lib/meshtasticd"
+ ],
+ "env": [
+ "MESHTASTIC_PORT=/dev/ttyUSB0",
+ "MESHTASTIC_SERIAL=true"
+ ],
+ "notes": "Requires a LoRa radio device at /dev/ttyUSB0. The config file is rendered from the app manifest before container start."
}
},
{
@@ -184,15 +277,19 @@
"dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine",
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
"containerConfig": {
- "ports": ["8082:80"],
- "volumes": ["/var/lib/archipelago/vaultwarden:/data"]
+ "ports": [
+ "8082:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/vaultwarden:/data"
+ ]
}
},
{
"id": "searxng",
"title": "SearXNG",
- "version": "2024.1.0",
- "description": "Privacy-respecting metasearch engine.",
+ "version": "1.0.0",
+ "description": "Privacy-respecting metasearch engine. Search the web without tracking.",
"icon": "/assets/img/app-icons/searxng.png",
"author": "SearXNG",
"category": "data",
@@ -200,21 +297,46 @@
"dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest",
"repoUrl": "https://github.com/searxng/searxng",
"containerConfig": {
- "ports": ["8888:8080"],
- "volumes": ["/var/lib/archipelago/searxng:/etc/searxng"]
+ "ports": [
+ "8888:8080"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/searxng:/etc/searxng"
+ ]
}
},
{
"id": "fedimint",
"title": "Fedimint",
"version": "0.10.0",
- "description": "Federated Bitcoin mint with privacy through federated guardians.",
+ "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.",
"icon": "/assets/img/app-icons/fedimint.png",
"author": "Fedimint",
"category": "money",
"dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0",
"repoUrl": "https://github.com/fedimint/fedimint"
},
+ {
+ "id": "fedimint-gateway",
+ "title": "Fedimint Gateway",
+ "version": "0.10.0",
+ "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.",
+ "icon": "/assets/img/app-icons/fedimint.png",
+ "author": "Fedimint",
+ "category": "money",
+ "dockerImage": "git.tx1138.com/lfg2025/gatewayd:v0.10.0",
+ "repoUrl": "https://github.com/fedimint/fedimint",
+ "containerConfig": {
+ "ports": [
+ "8176:8176",
+ "9737:9737"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/fedimint-gateway:/data",
+ "/var/lib/archipelago/lnd:/lnd:ro"
+ ]
+ }
+ },
{
"id": "jellyfin",
"title": "Jellyfin",
@@ -226,8 +348,13 @@
"dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13",
"repoUrl": "https://github.com/jellyfin/jellyfin",
"containerConfig": {
- "ports": ["8096:8096"],
- "volumes": ["/var/lib/archipelago/jellyfin/config:/config", "/var/lib/archipelago/jellyfin/cache:/cache"]
+ "ports": [
+ "8096:8096"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/jellyfin/config:/config",
+ "/var/lib/archipelago/jellyfin/cache:/cache"
+ ]
}
},
{
@@ -244,34 +371,47 @@
{
"id": "homeassistant",
"title": "Home Assistant",
- "version": "2024.1",
- "description": "Open-source home automation.",
+ "version": "2024.1.0",
+ "description": "Open source home automation platform. Control and monitor your smart home devices.",
"icon": "/assets/img/app-icons/homeassistant.png",
"author": "Home Assistant",
"category": "home",
"dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1",
"repoUrl": "https://github.com/home-assistant/core",
"containerConfig": {
- "ports": ["8123:8123"],
- "volumes": ["/var/lib/archipelago/home-assistant:/config"],
- "env": ["TZ=UTC"]
+ "ports": [
+ "8123:8123"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/home-assistant:/config"
+ ],
+ "env": [
+ "TZ=UTC"
+ ]
}
},
{
"id": "grafana",
"title": "Grafana",
"version": "10.2.0",
- "description": "Analytics and monitoring dashboards.",
+ "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.",
"icon": "/assets/img/app-icons/grafana.png",
"author": "Grafana Labs",
"category": "data",
"tier": "recommended",
- "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0",
+ "dockerImage": "grafana/grafana:10.2.0",
"repoUrl": "https://github.com/grafana/grafana",
"containerConfig": {
- "ports": ["3000:3000"],
- "volumes": ["/var/lib/archipelago/grafana:/var/lib/grafana"],
- "env": ["GF_PATHS_DATA=/var/lib/grafana", "GF_USERS_ALLOW_SIGN_UP=false"]
+ "ports": [
+ "3000:3000"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/grafana:/var/lib/grafana"
+ ],
+ "env": [
+ "GF_PATHS_DATA=/var/lib/grafana",
+ "GF_USERS_ALLOW_SIGN_UP=false"
+ ]
}
},
{
@@ -286,10 +426,42 @@
"dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable",
"repoUrl": "https://github.com/tailscale/tailscale",
"containerConfig": {
- "ports": ["8240:8240"],
- "volumes": ["/var/lib/archipelago/tailscale:/var/lib/tailscale"],
- "env": ["TS_STATE_DIR=/var/lib/tailscale"],
- "args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"]
+ "ports": [
+ "8240:8240"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/tailscale:/var/lib/tailscale"
+ ],
+ "env": [
+ "TS_STATE_DIR=/var/lib/tailscale"
+ ],
+ "args": [
+ "sh",
+ "-c",
+ "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait"
+ ]
+ }
+ },
+ {
+ "id": "portainer",
+ "title": "Portainer",
+ "version": "2.19.4",
+ "description": "Container management web UI for the local Podman socket.",
+ "icon": "/assets/img/app-icons/portainer.webp",
+ "author": "Portainer",
+ "category": "development",
+ "tier": "optional",
+ "dockerImage": "146.59.87.168:3000/lfg2025/portainer:latest",
+ "repoUrl": "https://github.com/portainer/portainer",
+ "containerConfig": {
+ "ports": [
+ "9000:9000"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/portainer:/data",
+ "/run/user/1000/podman/podman.sock:/var/run/docker.sock"
+ ],
+ "notes": "Uses the manifest-owned Podman socket bind mount preparation path."
}
},
{
@@ -304,8 +476,14 @@
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
"repoUrl": "https://github.com/netbirdio/netbird",
"containerConfig": {
- "ports": ["8087:80", "8086:80", "3478:3478/udp"],
- "volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
+ "ports": [
+ "8087:80",
+ "8086:80",
+ "3478:3478/udp"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/netbird:/var/lib/netbird"
+ ],
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
}
},
@@ -321,10 +499,20 @@
"dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1",
"repoUrl": "https://github.com/louislam/uptime-kuma",
"containerConfig": {
- "ports": ["3002:3001"],
- "volumes": ["/var/lib/archipelago/uptime-kuma:/app/data"],
- "env": ["TZ=UTC"],
- "args": ["--", "node", "server/server.js"]
+ "ports": [
+ "3002:3001"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/uptime-kuma:/app/data"
+ ],
+ "env": [
+ "TZ=UTC"
+ ],
+ "args": [
+ "--",
+ "node",
+ "server/server.js"
+ ]
}
},
{
@@ -338,24 +526,35 @@
"dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915",
"repoUrl": "https://github.com/photoprism/photoprism",
"containerConfig": {
- "ports": ["2342:2342"],
- "volumes": ["/var/lib/archipelago/photoprism:/photoprism/storage"],
- "env": ["PHOTOPRISM_ADMIN_PASSWORD=archipelago", "PHOTOPRISM_DEFAULT_LOCALE=en"]
+ "ports": [
+ "2342:2342"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/photoprism:/photoprism/storage"
+ ],
+ "env": [
+ "PHOTOPRISM_ADMIN_PASSWORD=archipelago",
+ "PHOTOPRISM_DEFAULT_LOCALE=en"
+ ]
}
},
{
"id": "nextcloud",
"title": "Nextcloud",
- "version": "28",
+ "version": "29",
"description": "Your own private cloud. File sync, calendars, contacts.",
"icon": "/assets/img/app-icons/nextcloud.webp",
"author": "Nextcloud",
"category": "data",
- "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28",
+ "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:29",
"repoUrl": "https://github.com/nextcloud/server",
"containerConfig": {
- "ports": ["8085:80"],
- "volumes": ["/var/lib/archipelago/nextcloud:/var/www/html"]
+ "ports": [
+ "8085:80"
+ ],
+ "volumes": [
+ "/var/lib/archipelago/nextcloud:/var/www/html"
+ ]
}
}
]
diff --git a/neode-ui/src/views/appSession/generatedAppSessionConfig.ts b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts
new file mode 100644
index 00000000..02c59a06
--- /dev/null
+++ b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts
@@ -0,0 +1,90 @@
+/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */
+
+export const GENERATED_APP_PORTS: Record = {
+ "aiui": 5180,
+ "archy-mempool-web": 4080,
+ "archy-nbxplorer": 32838,
+ "botfights": 9100,
+ "btcpay-server": 23000,
+ "did-wallet": 8083,
+ "electrumx": 50001,
+ "fedimint": 8175,
+ "filebrowser": 8083,
+ "gitea": 3001,
+ "grafana": 3000,
+ "homeassistant": 8123,
+ "indeedhub": 7778,
+ "jellyfin": 8096,
+ "lnd-ui": 18083,
+ "mempool": 4080,
+ "mempool-api": 8999,
+ "meshtastic": 4403,
+ "morphos-server": 8086,
+ "nextcloud": 8085,
+ "nostr-rs-relay": 18081,
+ "onlyoffice": 8088,
+ "photoprism": 2342,
+ "portainer": 9000,
+ "router": 8084,
+ "searxng": 8888,
+ "strfry": 8082,
+ "uptime-kuma": 3002,
+ "vaultwarden": 8082,
+ "web5-dwn": 3000,
+}
+
+export const GENERATED_APP_TITLES: Record = {
+ "aiui": "AI Assistant",
+ "archy-btcpay-db": "BTCPay Postgres",
+ "archy-mempool-db": "Mempool MariaDB",
+ "archy-mempool-web": "Mempool Web",
+ "archy-nbxplorer": "NBXplorer",
+ "bitcoin-core": "Bitcoin Core",
+ "bitcoin-knots": "Bitcoin Knots",
+ "bitcoin-ui": "Bitcoin UI",
+ "botfights": "BotFights",
+ "btcpay-server": "BTCPay Server",
+ "core-lightning": "Core Lightning (CLN)",
+ "did-wallet": "Web5 DID Wallet",
+ "electrs-ui": "Electrs UI",
+ "electrumx": "ElectrumX",
+ "fedimint": "Fedimint",
+ "fedimint-gateway": "Fedimint Gateway",
+ "filebrowser": "File Browser",
+ "gitea": "Gitea",
+ "grafana": "Grafana",
+ "homeassistant": "Home Assistant",
+ "indeedhub": "IndeeHub",
+ "jellyfin": "Jellyfin",
+ "lightning-stack": "Lightning Stack",
+ "lnd": "LND",
+ "lnd-ui": "LND UI",
+ "mempool": "Mempool Explorer",
+ "mempool-api": "Mempool API",
+ "meshtastic": "Meshtastic",
+ "morphos-server": "MorphOS Server",
+ "nextcloud": "Nextcloud",
+ "nostr-rs-relay": "Nostr Relay (Rust)",
+ "onlyoffice": "OnlyOffice",
+ "photoprism": "PhotoPrism",
+ "portainer": "Portainer",
+ "router": "Mesh Router",
+ "searxng": "SearXNG",
+ "strfry": "Strfry Nostr Relay",
+ "uptime-kuma": "Uptime Kuma",
+ "vaultwarden": "Vaultwarden",
+ "web5-dwn": "Decentralized Web Node",
+}
+
+export const GENERATED_NEW_TAB_APPS = new Set([
+ "btcpay-server",
+ "gitea",
+ "grafana",
+ "homeassistant",
+ "nextcloud",
+ "onlyoffice",
+ "photoprism",
+ "portainer",
+ "uptime-kuma",
+ "vaultwarden",
+])
diff --git a/scripts/check-app-catalog-drift.py b/scripts/check-app-catalog-drift.py
new file mode 100644
index 00000000..c7219d80
--- /dev/null
+++ b/scripts/check-app-catalog-drift.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+"""Report drift between app-catalog/catalog.json and apps/*/manifest.yml."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+INTERNAL_MANIFEST_IDS = {
+ "aiui",
+ "archy-btcpay-db",
+ "archy-mempool-db",
+ "archy-mempool-web",
+ "archy-nbxplorer",
+ "bitcoin-ui",
+ "core-lightning",
+ "did-wallet",
+ "electrs-ui",
+ "lightning-stack",
+ "lnd-ui",
+ "mempool-api",
+ "morphos-server",
+ "onlyoffice",
+ "router",
+ "strfry",
+ "web5-dwn",
+}
+
+LEGACY_STACK_CATALOG_IDS = {
+ "immich",
+ "netbird",
+ "saleor",
+ "tailscale",
+}
+
+
+def load_catalog(path: Path) -> dict[str, dict[str, Any]]:
+ with path.open("r", encoding="utf-8") as fh:
+ data = json.load(fh)
+ apps = data.get("apps", [])
+ if not isinstance(apps, list):
+ raise ValueError(f"{path}: expected .apps to be a list")
+ return {str(app.get("id", "")): app for app in apps if isinstance(app, dict) and app.get("id")}
+
+
+def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]:
+ manifests: dict[str, dict[str, Any]] = {}
+ for path in sorted(apps_dir.glob("*/manifest.yml")):
+ with path.open("r", encoding="utf-8") as fh:
+ data = yaml.safe_load(fh)
+ if not isinstance(data, dict) or not isinstance(data.get("app"), dict):
+ continue
+ app = data["app"]
+ app_id = app.get("id")
+ if app_id:
+ manifests[str(app_id)] = {"path": str(path), "app": app}
+ return manifests
+
+
+def metadata(app: dict[str, Any]) -> dict[str, Any]:
+ value = app.get("metadata")
+ return value if isinstance(value, dict) else {}
+
+
+def manifest_value(app: dict[str, Any], field: str) -> Any:
+ meta = metadata(app)
+ container = app.get("container") if isinstance(app.get("container"), dict) else {}
+ match field:
+ case "title":
+ return app.get("name")
+ case "version":
+ return str(app.get("version", ""))
+ case "description":
+ return app.get("description")
+ case "dockerImage":
+ return container.get("image")
+ case "category":
+ return app.get("category") or meta.get("category")
+ case "tier":
+ return meta.get("tier")
+ case "icon":
+ return meta.get("icon")
+ case "repoUrl":
+ return meta.get("repo") or meta.get("repoUrl")
+ case _:
+ return None
+
+
+def normalize(value: Any) -> str:
+ if value is None:
+ return ""
+ return str(value).strip()
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--catalog", default="app-catalog/catalog.json")
+ parser.add_argument("--apps-dir", default="apps")
+ parser.add_argument(
+ "--strict",
+ action="store_true",
+ help="exit non-zero when missing entries or metadata drift are found",
+ )
+ parser.add_argument(
+ "--release",
+ action="store_true",
+ help="suppress known internal/legacy-stack entries so output is release-actionable",
+ )
+ args = parser.parse_args()
+
+ catalog = load_catalog(Path(args.catalog))
+ manifests = load_manifests(Path(args.apps_dir))
+
+ catalog_ids = set(catalog)
+ manifest_ids = set(manifests)
+ missing_manifests = sorted(catalog_ids - manifest_ids)
+ missing_catalog = sorted(manifest_ids - catalog_ids)
+ if args.release:
+ missing_manifests = [app_id for app_id in missing_manifests if app_id not in LEGACY_STACK_CATALOG_IDS]
+ missing_catalog = [app_id for app_id in missing_catalog if app_id not in INTERNAL_MANIFEST_IDS]
+
+ compared_fields = [
+ "title",
+ "version",
+ "description",
+ "dockerImage",
+ "category",
+ "tier",
+ "icon",
+ "repoUrl",
+ ]
+ drift: list[str] = []
+ for app_id in sorted(catalog_ids & manifest_ids):
+ catalog_app = catalog[app_id]
+ manifest_app = manifests[app_id]["app"]
+ for field in compared_fields:
+ catalog_val = normalize(catalog_app.get(field))
+ manifest_val = normalize(manifest_value(manifest_app, field))
+ if catalog_val and manifest_val and catalog_val != manifest_val:
+ drift.append(f"{app_id}: {field}: catalog={catalog_val!r} manifest={manifest_val!r}")
+
+ print(
+ json.dumps(
+ {
+ "catalog_apps": len(catalog),
+ "manifest_apps": len(manifests),
+ "missing_manifests": len(missing_manifests),
+ "missing_catalog": len(missing_catalog),
+ "metadata_drift": len(drift),
+ },
+ sort_keys=True,
+ )
+ )
+
+ for app_id in missing_manifests:
+ print(f"MISSING_MANIFEST {app_id}")
+ for app_id in missing_catalog:
+ print(f"MISSING_CATALOG {app_id}")
+ for item in drift:
+ print(f"DRIFT {item}")
+
+ if args.strict and (missing_manifests or missing_catalog or drift):
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/generate-app-catalog.py b/scripts/generate-app-catalog.py
new file mode 100644
index 00000000..9d46f3ab
--- /dev/null
+++ b/scripts/generate-app-catalog.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+"""Sync public app catalog metadata from apps/*/manifest.yml.
+
+Manifests are the source of truth for fields the runtime already needs
+(`name`, `version`, `description`, container image, category, tier, icon,
+repo URL). The catalog still owns presentation-only fields that manifests do
+not carry yet, such as `author`, `requires`, `featured`, and rich
+`containerConfig` notes.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+
+SYNC_FIELDS = ("title", "version", "description", "dockerImage", "category", "tier", "icon", "repoUrl")
+
+
+def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]:
+ manifests: dict[str, dict[str, Any]] = {}
+ for path in sorted(apps_dir.glob("*/manifest.yml")):
+ with path.open("r", encoding="utf-8") as fh:
+ data = yaml.safe_load(fh)
+ if not isinstance(data, dict) or not isinstance(data.get("app"), dict):
+ continue
+ app = data["app"]
+ app_id = app.get("id")
+ if app_id:
+ manifests[str(app_id)] = app
+ return manifests
+
+
+def metadata(app: dict[str, Any]) -> dict[str, Any]:
+ value = app.get("metadata")
+ return value if isinstance(value, dict) else {}
+
+
+def manifest_catalog_values(app: dict[str, Any]) -> dict[str, str]:
+ meta = metadata(app)
+ container = app.get("container") if isinstance(app.get("container"), dict) else {}
+ values = {
+ "title": app.get("name"),
+ "version": app.get("version"),
+ "description": app.get("description"),
+ "dockerImage": container.get("image"),
+ "category": app.get("category") or meta.get("category"),
+ "tier": meta.get("tier"),
+ "icon": meta.get("icon"),
+ "repoUrl": meta.get("repo") or meta.get("repoUrl") or meta.get("source"),
+ }
+ return {key: str(value) for key, value in values.items() if value is not None and str(value).strip()}
+
+
+def manifest_launch_port(app: dict[str, Any]) -> int | None:
+ """Return the manifest-owned public UI port, when it is unambiguous."""
+ interfaces = app.get("interfaces")
+ if isinstance(interfaces, dict):
+ main = interfaces.get("main")
+ if isinstance(main, dict) and main.get("type") == "ui":
+ port = main.get("port")
+ if isinstance(port, int):
+ return port
+ if isinstance(port, str) and port.isdigit():
+ return int(port)
+
+ ports = app.get("ports")
+ if not isinstance(ports, list):
+ return None
+ tcp_ports = [
+ item.get("host")
+ for item in ports
+ if isinstance(item, dict) and str(item.get("protocol", "tcp")).lower() == "tcp"
+ ]
+ if len(tcp_ports) != 1:
+ return None
+ port = tcp_ports[0]
+ if isinstance(port, int):
+ return port
+ if isinstance(port, str) and port.isdigit():
+ return int(port)
+ return None
+
+
+def manifest_opens_in_new_tab(app: dict[str, Any]) -> bool:
+ """Return whether manifest launch metadata opts the app out of iframe launch."""
+ launch = metadata(app).get("launch")
+ if not isinstance(launch, dict):
+ return False
+ return launch.get("open_in_new_tab") is True
+
+
+def ts_string(value: str) -> str:
+ return json.dumps(value, ensure_ascii=True)
+
+
+def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str:
+ ports: dict[str, int] = {}
+ titles: dict[str, str] = {}
+ new_tab_apps: list[str] = []
+ for app_id, app in sorted(manifests.items()):
+ name = app.get("name")
+ if isinstance(name, str) and name.strip():
+ titles[app_id] = name.strip()
+ port = manifest_launch_port(app)
+ if port:
+ ports[app_id] = port
+ if manifest_opens_in_new_tab(app):
+ new_tab_apps.append(app_id)
+
+ lines = [
+ "/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */",
+ "",
+ "export const GENERATED_APP_PORTS: Record = {",
+ ]
+ for app_id, port in ports.items():
+ lines.append(f" {ts_string(app_id)}: {port},")
+ lines.extend([
+ "}",
+ "",
+ "export const GENERATED_APP_TITLES: Record = {",
+ ])
+ for app_id, title in titles.items():
+ lines.append(f" {ts_string(app_id)}: {ts_string(title)},")
+ lines.extend([
+ "}",
+ "",
+ "export const GENERATED_NEW_TAB_APPS = new Set([",
+ ])
+ for app_id in new_tab_apps:
+ lines.append(f" {ts_string(app_id)},")
+ lines.extend(["])", ""])
+ return "\n".join(lines)
+
+
+def sync_catalog(path: Path, manifests: dict[str, dict[str, Any]]) -> int:
+ with path.open("r", encoding="utf-8") as fh:
+ catalog = json.load(fh)
+ apps = catalog.get("apps")
+ if not isinstance(apps, list):
+ raise ValueError(f"{path}: expected .apps to be a list")
+
+ changed = 0
+ for catalog_app in apps:
+ if not isinstance(catalog_app, dict):
+ continue
+ app_id = catalog_app.get("id")
+ if not app_id or str(app_id) not in manifests:
+ continue
+ values = manifest_catalog_values(manifests[str(app_id)])
+ for field in SYNC_FIELDS:
+ if field not in values:
+ continue
+ old = catalog_app.get(field)
+ new = values[field]
+ if old != new:
+ catalog_app[field] = new
+ changed += 1
+
+ path.write_text(json.dumps(catalog, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
+ return changed
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--apps-dir", default="apps")
+ parser.add_argument(
+ "--catalog",
+ action="append",
+ default=[],
+ help="Catalog JSON path to update. May be passed multiple times.",
+ )
+ parser.add_argument(
+ "--app-session-config",
+ default="neode-ui/src/views/appSession/generatedAppSessionConfig.ts",
+ help="Generated TypeScript app-session metadata path. Pass an empty string to skip.",
+ )
+ args = parser.parse_args()
+
+ catalogs = args.catalog or ["app-catalog/catalog.json", "neode-ui/public/catalog.json"]
+ manifests = load_manifests(Path(args.apps_dir))
+ total = 0
+ for catalog in catalogs:
+ changed = sync_catalog(Path(catalog), manifests)
+ total += changed
+ print(f"{catalog}: updated {changed} fields")
+ if args.app_session_config:
+ path = Path(args.app_session_config)
+ content = render_app_session_config(manifests)
+ old = path.read_text(encoding="utf-8") if path.exists() else ""
+ if old != content:
+ path.write_text(content, encoding="utf-8")
+ print(f"{path}: updated")
+ else:
+ print(f"{path}: updated 0 fields")
+ print(f"total_updated={total}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())