fix(apps): stabilize btcpay and public proxy launch flows

This commit is contained in:
archipelago
2026-05-19 09:26:43 -04:00
parent e9898ead76
commit d736364ad7
22 changed files with 322 additions and 114 deletions

View File

@@ -510,6 +510,22 @@ fix_missing_rootless_ports() {
$fixed && return 0 || return 1
}
# ── Fix 11: Nginx Proxy Manager public host bridge ───────────
# Host nginx owns public 80/443 on Archipelago. Mirror NPM proxy hosts into
# host nginx so issued certs and public traffic reach the intended upstreams.
fix_npm_public_hosts() {
local script="/opt/archipelago/scripts/sync-npm-public-hosts.sh"
[ -x "$script" ] || script="$SCRIPT_DIR/sync-npm-public-hosts.sh"
[ -x "$script" ] || return 1
[ -f /var/lib/archipelago/nginx-proxy-manager/data/database.sqlite ] || return 1
if "$script" >/dev/null 2>&1; then
log "Synced Nginx Proxy Manager public hosts into host nginx"
return 0
fi
return 1
}
# ── Main ─────────────────────────────────────────────────────
# If remote host provided, run via SSH
@@ -539,6 +555,7 @@ run_fix "exit-127" fix_exit_127
run_fix "netns-egress" fix_rootless_netns_egress
run_fix "stopped-core" fix_stopped_core_containers
run_fix "rootless-ports" fix_missing_rootless_ports
run_fix "npm-public-hosts" fix_npm_public_hosts
echo ""
if [ $FIXES_APPLIED -gt 0 ]; then

View File

@@ -273,7 +273,7 @@ load_spec_btcpay-server() {
SPEC_PORTS="23000:49392"
SPEC_VOLUMES="/var/lib/archipelago/btcpay:/datadir"
SPEC_MEMORY="$(mem_limit btcpay-server)"
SPEC_HEALTH_CMD="curl -sf http://localhost:49392/ || exit 1"
SPEC_HEALTH_CMD="bash -ec '</dev/tcp/127.0.0.1/49392'"
SPEC_ENV="ASPNETCORE_URLS=http://0.0.0.0:49392 BTCPAY_PROTOCOL=http BTCPAY_HOST=$HOST_IP:23000 BTCPAY_CHAINS=btc BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS BTCPAY_POSTGRES=Username=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay"
SPEC_TIER="2"
SPEC_DATA_DIR="/var/lib/archipelago/btcpay"

View File

@@ -625,7 +625,7 @@ deploy_node() {
echo ' Creating btcpay-server...'
sudo mkdir -p /var/lib/archipelago/btcpay
\$DOCKER run -d --name btcpay-server --restart unless-stopped \$NET_OPT \
--health-cmd 'curl -sf http://localhost:49392/' --health-interval=30s --health-timeout=10s --health-retries=3 \
--health-cmd "bash -ec '</dev/tcp/127.0.0.1/49392'" --health-interval=30s --health-timeout=10s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \
@@ -909,7 +909,8 @@ LNDCONF
if \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx nginx-proxy-manager; then
\$DOCKER start nginx-proxy-manager 2>/dev/null || true
else
sudo mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt
sudo mkdir -p /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge /var/lib/archipelago/nginx-proxy-manager/letsencrypt
sudo chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
\$DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \
--health-cmd 'curl -sf http://localhost:81/' --health-interval=30s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \

View File

@@ -481,10 +481,13 @@ log "archy-net network ready"
log "Fixing rootless podman UID mapping..."
# Containers running as root (UID 0 → host UID 100000)
for dir in lnd electrumx btcpay nbxplorer jellyfin vaultwarden \
home-assistant fedimint fedimint-gateway photoprism ollama filebrowser \
nextcloud uptime-kuma nginx-proxy-manager portainer nostr-rs-relay; do
home-assistant fedimint fedimint-gateway photoprism ollama filebrowser \
nextcloud uptime-kuma portainer nostr-rs-relay; do
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100000:100000 "/var/lib/archipelago/$dir" 2>/dev/null
done
# Nginx Proxy Manager runs as root in the rootless user namespace, which maps to
# the archipelago user on host bind mounts. Keep certbot's webroot writable.
[ -d /var/lib/archipelago/nginx-proxy-manager ] && chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null
# Bitcoin Knots: container UID 101 → host UID 100101
[ -d /var/lib/archipelago/bitcoin ] && chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null
# Postgres: container UID 70 → host UID 100070
@@ -875,7 +878,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
log "Creating BTCPay Server..."
mkdir -p /var/lib/archipelago/btcpay/Main
$DOCKER run -d --name btcpay-server --restart unless-stopped \
--health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--health-cmd="bash -ec '</dev/tcp/127.0.0.1/49392'" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit btcpay-server) --network archy-net --network-alias btcpay-server \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
@@ -1181,6 +1184,8 @@ track_container "filebrowser"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
log "Creating Nginx Proxy Manager..."
mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt
mkdir -p /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge
chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
NPM_ADMIN_PORT=$(alloc_port nginx-proxy-manager 8081)
NPM_HTTP_PORT=$(alloc_port nginx-proxy-manager-http 8084)
NPM_HTTPS_PORT=$(alloc_port nginx-proxy-manager-https 8444)

View File

@@ -24,7 +24,7 @@ MEMPOOL_WEB_IMAGE="$ARCHY_REGISTRY/mempool-frontend:v3.0.0"
MARIADB_IMAGE="$ARCHY_REGISTRY/mariadb:11.4.10"
# BTCPay
BTCPAY_IMAGE="$ARCHY_REGISTRY/btcpayserver:1.13.7"
BTCPAY_IMAGE="docker.io/btcpayserver/btcpayserver:2.3.9"
NBXPLORER_IMAGE="$ARCHY_REGISTRY/nbxplorer:2.6.0"
POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
BTCPAY_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"

View File

@@ -186,7 +186,7 @@ fi
# for backward compatibility with older binaries that still look there.
SCRIPTS_DEST="/opt/archipelago/scripts"
sudo mkdir -p "$SCRIPTS_DEST"
for script in image-versions.sh reconcile-containers.sh container-specs.sh container-doctor.sh app-surface-smoke-test.sh bitcoin-stack-lifecycle-test.sh; do
for script in image-versions.sh reconcile-containers.sh container-specs.sh container-doctor.sh sync-npm-public-hosts.sh app-surface-smoke-test.sh bitcoin-stack-lifecycle-test.sh; do
src="$REPO_DIR/scripts/$script"
if [ -f "$src" ]; then
sudo install -m 755 "$src" "$SCRIPTS_DEST/$script"

View File

@@ -0,0 +1,119 @@
#!/bin/bash
set -euo pipefail
DB="/var/lib/archipelago/nginx-proxy-manager/data/database.sqlite"
OUT="/etc/nginx/conf.d/public-npm-proxy-hosts.conf"
ACME_ROOT="/var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge"
LE_ROOT="/var/lib/archipelago/nginx-proxy-manager/letsencrypt/live"
[ -f "$DB" ] || exit 0
mkdir -p "$ACME_ROOT/.well-known/acme-challenge"
chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
python3 - "$DB" "$ACME_ROOT" "$LE_ROOT" >"$tmp" <<'PY'
import json
import os
import sqlite3
import sys
db, acme_root, le_root = sys.argv[1:]
con = sqlite3.connect(db)
con.row_factory = sqlite3.Row
rows = con.execute(
"""
select p.id, p.domain_names, p.forward_scheme, p.forward_host, p.forward_port,
p.certificate_id, p.ssl_forced, c.provider
from proxy_host p
left join certificate c on c.id = p.certificate_id
where p.enabled = 1 and p.certificate_id > 0
order by p.id
"""
).fetchall()
print("# Generated by sync-npm-public-hosts.sh; do not edit by hand.")
for row in rows:
try:
domains = [d for d in json.loads(row["domain_names"] or "[]") if d]
except Exception:
domains = []
if not domains:
continue
cert_id = row["certificate_id"]
cert = f"{le_root}/npm-{cert_id}/fullchain.pem"
key = f"{le_root}/npm-{cert_id}/privkey.pem"
if row["provider"] != "letsencrypt":
continue
if not os.path.isfile(cert) or not os.path.isfile(key):
continue
names = " ".join(domains)
scheme = row["forward_scheme"] or "http"
host = row["forward_host"]
port = row["forward_port"]
if not host or not port:
continue
# NPM containers use this name to reach host-published services; host nginx
# itself should use loopback for the same services.
nginx_host = "127.0.0.1" if host == "host.containers.internal" else host
print(f"""
server {{
listen 80;
server_name {names};
location ^~ /.well-known/acme-challenge/ {{
default_type text/plain;
root {acme_root};
try_files $uri =404;
}}
location / {{
return 301 https://$host$request_uri;
}}
}}
server {{
listen 443 ssl;
server_name {names};
ssl_certificate {cert};
ssl_certificate_key {key};
location / {{
proxy_pass {scheme}://{nginx_host}:{port};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Scheme https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}}
}}
""")
PY
backup=""
if [ -f "$OUT" ]; then
backup=$(mktemp)
cp "$OUT" "$backup"
fi
restore_previous() {
if [ -n "$backup" ] && [ -f "$backup" ]; then
install -m 0644 "$backup" "$OUT"
else
rm -f "$OUT"
fi
}
if ! install -m 0644 "$tmp" "$OUT" || ! nginx -t >/dev/null; then
restore_previous
nginx -t >/dev/null 2>&1 || true
exit 1
fi
systemctl reload nginx