fix: recover mempool frontend after backend address changes
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
# Archipelago mempool frontend — adds a resilient nginx backend proxy.
|
||||
#
|
||||
# The only delta vs the upstream image is /patch/entrypoint.sh, which rewrites
|
||||
# the generated nginx-mempool.conf to use `resolver` + a variable proxy_pass so
|
||||
# the frontend re-resolves the backend (mempool-api) via DNS on every request.
|
||||
# Without this, nginx pins the backend IP at startup and serves 502 / "offline"
|
||||
# after any backend restart (podman reassigns the IP). See the script header.
|
||||
ARG BASE=source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.0.0
|
||||
# Keep the upstream startup logic; repair its rendered proxy configuration.
|
||||
# Publish this derived image under an Archipelago-specific tag, never the
|
||||
# upstream version tag that the registry mirror can overwrite.
|
||||
ARG BASE=source.archipelago-foundation.org/lfg2025/mempool-frontend@sha256:d63498a109622475c913db4e3199d893f2440a451450e542923d2e55a38407a0
|
||||
FROM ${BASE}
|
||||
|
||||
# --chmod keeps the exec bit (build runs as USER 1000, plain COPY lands root:0644
|
||||
# → "not executable"). Base USER/ENTRYPOINT/CMD (1000 / /patch/entrypoint.sh /
|
||||
# nginx -g "daemon off;") are inherited unchanged.
|
||||
COPY --chmod=0755 entrypoint.sh /patch/entrypoint.sh
|
||||
RUN cp /patch/entrypoint.sh /patch/upstream-entrypoint.sh
|
||||
COPY --chmod=0755 entrypoint.sh start-nginx.sh repair-nginx.sh /patch/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Mempool frontend DNS recovery
|
||||
|
||||
The stock v3.3.1 nginx configuration resolves `mempool-api` only when workers
|
||||
start. Recreating the backend can change its Podman address while the frontend
|
||||
continues to serve its static page, leaving all API/WebSocket requests offline.
|
||||
|
||||
Build and test the derived image before publishing:
|
||||
|
||||
```sh
|
||||
podman build --pull=never -t source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1-archy1 docker/mempool-frontend
|
||||
python3 scripts/test-mempool-dns-recovery.py
|
||||
```
|
||||
|
||||
The base is pinned by digest. The wrapper preserves upstream runtime options,
|
||||
then repairs all four local API/WebSocket routes after placeholder rendering.
|
||||
DNS is cached for five seconds using the container network resolver. Explicit
|
||||
rewrites preserve API prefixes and query arguments; backend absence does not
|
||||
prevent nginx startup. An unexpected upstream configuration fails startup
|
||||
instead of silently omitting the fix.
|
||||
|
||||
Use an Archipelago-specific image tag. Do not replace it with a stock upstream
|
||||
mirror when updating mempool. Every upstream update must rebuild this wrapper
|
||||
and pass the recovery test (backend absent, changed IP, HTTP and WebSocket
|
||||
mapping, repeated repair, and frontend restart).
|
||||
|
||||
Publish the tested image before publishing the signed app catalog. Both the
|
||||
mempool umbrella image mapping and the archy-mempool-web embedded manifest must
|
||||
point at the patched image. Keep scripts/image-versions.sh in sync. The frontend
|
||||
health check must reach `/api/v1/backend-info` through nginx, not only `/`.
|
||||
Regular → Executable
+4
-136
@@ -1,137 +1,5 @@
|
||||
#!/bin/sh
|
||||
__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__=${BACKEND_MAINNET_HTTP_HOST:=127.0.0.1}
|
||||
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
|
||||
__MEMPOOL_FRONTEND_HTTP_PORT__=${FRONTEND_HTTP_PORT:=8080}
|
||||
|
||||
CONF=/etc/nginx/conf.d/nginx-mempool.conf
|
||||
|
||||
# ─── archipelago patch ────────────────────────────────────────────────────
|
||||
# The stock frontend writes `proxy_pass http://<backend>:8999` with a literal
|
||||
# hostname and NO resolver, so nginx resolves the backend IP ONCE at worker
|
||||
# start and caches it for the process lifetime. Podman reassigns the backend
|
||||
# container's IP whenever it is restarted/recreated (gate, OTA, crash, reboot
|
||||
# re-IPAM), after which nginx keeps proxying to the dead IP → /api hangs, the
|
||||
# websocket 502s, and the mempool UI shows "offline" until nginx is reloaded.
|
||||
#
|
||||
# Fix: force per-request DNS re-resolution via `resolver` + a variable in
|
||||
# proxy_pass. Because a variable in proxy_pass disables nginx's automatic
|
||||
# location→URI rewriting, each block is rewritten to preserve its original
|
||||
# path mapping exactly:
|
||||
# /api/v1/ws, /ws → "/" (var + "/" replaces the whole URI)
|
||||
# /api/v1 → identity (no-URI proxy_pass passes $uri unchanged)
|
||||
# /api/ → /api/v1/$1 (explicit rewrite, then no-URI proxy_pass)
|
||||
# Operates on the __PLACEHOLDER__ tokens so the host/port sed below fills in
|
||||
# the concrete values (incl. the `set $mp_backend` line). Idempotent.
|
||||
# Resolver address: podman's aardvark-dns answers on the network gateway
|
||||
# (e.g. 10.89.0.1), NOT Docker's 127.0.0.11. Read it from resolv.conf so this
|
||||
# works on any podman network/subnet (and still falls back for Docker).
|
||||
ARCHY_RESOLVER=$(awk '/^nameserver/ { print $2; exit }' /etc/resolv.conf 2>/dev/null)
|
||||
ARCHY_RESOLVER=${ARCHY_RESOLVER:-127.0.0.11}
|
||||
|
||||
if ! grep -q 'set \$mp_backend' "$CONF"; then
|
||||
awk -v res_addr="$ARCHY_RESOLVER" '
|
||||
BEGIN { res = 0 }
|
||||
/^[[:space:]]*location / && res == 0 {
|
||||
print "\tresolver " res_addr " valid=10s ipv6=off;"
|
||||
res = 1
|
||||
}
|
||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/;/ {
|
||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/;"
|
||||
next
|
||||
}
|
||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/api\/v1\/;/ {
|
||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
||||
print "\t\trewrite ^/api/(.*)$ /api/v1/$1 break;"
|
||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__;"
|
||||
next
|
||||
}
|
||||
/proxy_pass http:\/\/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__\/api\/v1;/ {
|
||||
print "\t\tset $mp_backend __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__;"
|
||||
print "\t\tproxy_pass http://$mp_backend:__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__;"
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$CONF" > "$CONF.archy" && mv "$CONF.archy" "$CONF"
|
||||
fi
|
||||
# ─── end archipelago patch ────────────────────────────────────────────────
|
||||
|
||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__/${__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
||||
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" /etc/nginx/conf.d/nginx-mempool.conf
|
||||
|
||||
cp /etc/nginx/nginx.conf /patch/nginx.conf
|
||||
sed -i "s/__MEMPOOL_FRONTEND_HTTP_PORT__/${__MEMPOOL_FRONTEND_HTTP_PORT__}/g" /patch/nginx.conf
|
||||
cat /patch/nginx.conf > /etc/nginx/nginx.conf
|
||||
|
||||
if [ "${LIGHTNING_DETECTED_PORT}" != "" ];then
|
||||
export LIGHTNING=true
|
||||
fi
|
||||
|
||||
# Runtime overrides - read env vars defined in docker compose
|
||||
|
||||
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
|
||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__TESTNET4_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
|
||||
__ITEMS_PER_PAGE__=${ITEMS_PER_PAGE:=10}
|
||||
__KEEP_BLOCKS_AMOUNT__=${KEEP_BLOCKS_AMOUNT:=8}
|
||||
__NGINX_PROTOCOL__=${NGINX_PROTOCOL:=http}
|
||||
__NGINX_HOSTNAME__=${NGINX_HOSTNAME:=localhost}
|
||||
__NGINX_PORT__=${NGINX_PORT:=8999}
|
||||
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
||||
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
||||
__ROOT_NETWORK__=${ROOT_NETWORK:=}
|
||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||
__LIGHTNING__=${LIGHTNING:=false}
|
||||
__AUDIT__=${AUDIT:=false}
|
||||
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
|
||||
# Export as environment variables to be used by envsubst
|
||||
export __MAINNET_ENABLED__
|
||||
export __TESTNET_ENABLED__
|
||||
export __TESTNET4_ENABLED__
|
||||
export __SIGNET_ENABLED__
|
||||
export __LIQUID_ENABLED__
|
||||
export __LIQUID_TESTNET_ENABLED__
|
||||
export __ITEMS_PER_PAGE__
|
||||
export __KEEP_BLOCKS_AMOUNT__
|
||||
export __NGINX_PROTOCOL__
|
||||
export __NGINX_HOSTNAME__
|
||||
export __NGINX_PORT__
|
||||
export __BLOCK_WEIGHT_UNITS__
|
||||
export __MEMPOOL_BLOCKS_AMOUNT__
|
||||
export __BASE_MODULE__
|
||||
export __ROOT_NETWORK__
|
||||
export __MEMPOOL_WEBSITE_URL__
|
||||
export __LIQUID_WEBSITE_URL__
|
||||
export __MINING_DASHBOARD__
|
||||
export __LIGHTNING__
|
||||
export __AUDIT__
|
||||
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __ACCELERATOR_BUTTON__
|
||||
export __SERVICES_API__
|
||||
export __PUBLIC_ACCELERATIONS__
|
||||
export __HISTORICAL_PRICE__
|
||||
export __ADDITIONAL_CURRENCIES__
|
||||
|
||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||
echo ${folder}
|
||||
envsubst < ${folder}/config.template.js > ${folder}/config.js
|
||||
|
||||
exec "$@"
|
||||
set -eu
|
||||
# Preserve the pinned upstream entrypoint (including new runtime options).
|
||||
# Apply our DNS repair only after it has rendered the nginx configuration.
|
||||
exec /patch/upstream-entrypoint.sh /patch/start-nginx.sh "$@"
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
# Resolve the backend again after container IP changes. Run after upstream
|
||||
# placeholder substitution, so the repair also works on an existing container.
|
||||
set -eu
|
||||
conf=${1:-/etc/nginx/conf.d/nginx-mempool.conf}
|
||||
resolv=${2:-/etc/resolv.conf}
|
||||
backend=${BACKEND_MAINNET_HTTP_HOST:-127.0.0.1}
|
||||
port=${BACKEND_MAINNET_HTTP_PORT:-8999}
|
||||
resolver=$(awk '/^nameserver/ { print $2; exit }' "$resolv")
|
||||
[ -n "$resolver" ] || { echo 'No DNS resolver configured' >&2; exit 1; }
|
||||
case "$resolver" in *:*) resolver="[$resolver]" ;; esac
|
||||
case "$backend" in *[!a-zA-Z0-9._-]*|'') echo 'Invalid backend hostname' >&2; exit 1 ;; esac
|
||||
case "$port" in *[!0-9]*|'') echo 'Invalid backend port' >&2; exit 1 ;; esac
|
||||
|
||||
tmp=$(mktemp "${conf}.archy.XXXXXX")
|
||||
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
||||
awk -v backend="$backend" -v port="$port" -v resolver="$resolver" '
|
||||
BEGIN {
|
||||
base = "http://" backend ":" port
|
||||
print "# Archipelago: refresh backend DNS after container replacement."
|
||||
print "resolver " resolver " valid=5s ipv6=off; # archy-dns"
|
||||
print "resolver_timeout 3s; # archy-dns"
|
||||
}
|
||||
/# Archipelago: refresh backend DNS/ || /# archy-dns/ { next }
|
||||
/^[[:space:]]*location[[:space:]]/ { location = $2 }
|
||||
/^[[:space:]]*proxy_pass[[:space:]]/ && index($2, base) == 1 {
|
||||
target = $2
|
||||
sub(/;$/, "", target)
|
||||
path = substr(target, length(base) + 1)
|
||||
if (location != "/api/v1/ws" && location != "/ws" && location != "/api/v1" && location != "/api/") {
|
||||
print "Unexpected backend location: " location > "/dev/stderr"
|
||||
failed = 1; exit 1
|
||||
}
|
||||
if (path != "/" && path != "/api/v1" && path != "/api/v1/") {
|
||||
print "Unexpected backend URI mapping" > "/dev/stderr"
|
||||
failed = 1; exit 1
|
||||
}
|
||||
# Explicitly preserve prefix substitution and query arguments. A variable
|
||||
# proxy_pass without a URI forwards the rewritten URI and original args.
|
||||
print "\t\tset $mp_backend " backend ";"
|
||||
if (path != location)
|
||||
print "\t\trewrite ^" location "(.*)$ " path "$1 break;"
|
||||
print "\t\tproxy_pass http://$mp_backend:" port ";"
|
||||
count++
|
||||
next
|
||||
}
|
||||
/proxy_pass http:\/\/\$mp_backend:/ { count++ }
|
||||
{ print }
|
||||
END {
|
||||
if (failed || count != 4) {
|
||||
print "Expected four backend proxies; refusing an incomplete DNS repair" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$conf" > "$tmp"
|
||||
cat "$tmp" > "$conf"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
/patch/repair-nginx.sh
|
||||
nginx -t
|
||||
exec "$@"
|
||||
Reference in New Issue
Block a user