services: # umbrelOS genereert deze service zelf; wij vullen alleen in waar hij heen moet # wijzen. De hostnaam heeft de vorm __1. app_proxy: environment: APP_HOST: whatsnext-electrum-gate_server_1 APP_PORT: 80 # De agent. Hij schrijft status.json, leest de certificaten, bevraagt de # Electrum-server en neemt de certificaatkeuze aan. # # Waarom een tweede container en niet een shell-lus in de server hieronder: # dit werk is inmiddels een programma. Een certificaatdatum uitlezen, een # JSON-RPC-verzoek doen, een geschiedenis bijhouden en een keuze valideren zijn # geen dingen die je met openssl en nc aan elkaar knoopt zonder dat het stil # verkeerde antwoorden gaat geven. Bijkomend voordeel: de app hangt niet meer # af van de vraag of die twee gereedschappen in de nginx-image zitten. agent: # TODO (fase 4 van het plan Appstore): pinnen op de multi-arch index-digest, # te bepalen met `docker buildx imagetools inspect python:3-alpine` op de # Umbrel. Geldt voor beide images in dit bestand. image: python:3-alpine restart: on-failure environment: # Het adres van de Electrum-server die de gebruiker in umbrelOS gekozen # heeft. umbrelOS vult dit in op grond van de afhankelijkheid hieronder, # dus dit werkt met Electrs, Fulcrum en ElectrumX. GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP} GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT} GATE_TLS_PORT: "50022" # De zelfcontrole: de agent verbindt met de TLS-poort van de server # hieronder en maakt de handdruk af. De servicenaam resolveert binnen het # compose-netwerk, en dat is dezelfde weg die nginx andersom gebruikt met # zijn proxy_pass naar 'agent'. Niet localhost: dat is een andere container. GATE_TLS_HOST: server # Niet elke ronde meten, want elke meting is voor nginx een gewone sessie # en levert dus een regel in het activiteitenlog op. Vijf minuten is vaak # genoeg voor een waarde die zelden verandert; na een certificaatwissel # wordt er sowieso gemeten. GATE_SELF_CHECK_INTERVAL: "300" GATE_INTERVAL: "60" GATE_API_PORT: "8000" GATE_STATE_DIR: /var/lib/gate # Bron-naam=map. Een bron die niet gemount is, bestaat niet en wordt # overgeslagen; dat is de normale toestand voor een reverse proxy die de # gebruiker niet draait. GATE_CERT_SOURCES: "Zoraxy=/certs/zoraxy,Own folder=/certs/own" # De enige beschrijfbare bron, en dus de map waarin een upload van de # pagina landt. Expliciet en niet afgeleid uit de lijst hierboven: welke # bron beschrijfbaar is, hoort te staan naast de mount die dat toestaat. # Leeg zetten schakelt uploaden uit. GATE_UPLOAD_DIR: /certs/own PYTHONUNBUFFERED: "1" volumes: # Door umbrelOS ingevuld uit agent.py.template bij het starten. Er staat # geen accolade-variabele in dat bestand, dus de invulling laat de # Python-code ongemoeid; de agent leest zijn instellingen uit de omgeving # hierboven. De test in tests/ controleert die aanname. - ${APP_DATA_DIR}/agent.py:/app/agent.py:ro # De gedeelde toestand: status.json, cert.conf, de herlaadvlag en de # gekozen certificaat-id. Beide containers zitten hierin. # # Onder data/ en niet naast de templates, want dat is wat andere apps doen: # electrs mount ${APP_DATA_DIR}/data/electrs, mempool ${APP_DATA_DIR}/data. # Verplaatst op 20-08-2026 met het oog op publicatie in de officiele store. - ${APP_DATA_DIR}/data/runtime:/var/lib/gate # De certificaatbronnen. Zoraxy alleen lezen: dat zijn de certificaten van # een andere app en die raakt deze app niet aan. - ${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs:/certs/zoraxy:ro # De eigen map is de enige die beschrijfbaar is, want hier landt een upload # van de pagina. Alleen de agent; de nginx-container houdt hem alleen-lezen, # want die hoeft er nooit iets neer te zetten. - ${APP_DATA_DIR}/data/certs:/certs/own # # Nginx Proxy Manager hoort hier als derde bron bij en staat er bewust nog # niet in. Het pad hieronder is een gok en niet geverifieerd, en een # bind-mount naar een pad dat niet bestaat laat Docker het aanmaken. Dat zou # een lege maphierarchie neerzetten in de app-data van een app die # misschien niet eens geinstalleerd is, en die rommel blijft daar staan. # Eerst op de Umbrel controleren waar NPM zijn certificaten neerzet, dan # deze regel aanzetten en de naam toevoegen aan GATE_CERT_SOURCES. # # - ${UMBREL_ROOT}/app-data/nginx-proxy-manager/data/letsencrypt/live:/certs/npm:ro command: - python - /app/agent.py server: image: nginx:alpine restart: on-failure # De agent moet er zijn voordat nginx start, want de proxy_pass naar # http://agent:8000 wordt bij het starten opgelost en een onbekende naam # laat nginx afbreken. Op de cert.conf van de agent wordt niet gewacht: de # pagina komt hoe dan ook omhoog, zie het command-blok hieronder. depends_on: - agent ports: # De TLS-poort voor Electrum-wallets. Dit is de enige poort die deze app # zelf publiceert; de web-UI loopt via app_proxy. # # 50022 en niet de conventionele 50002, omdat Fulcrum die op de host # bezet: met Fulcrum als backend zou de container niet starten, en dat is # precies het omschakelen dat deze app moet ondersteunen. De poort naar # buiten is toch al een andere, want die staat in de router doorgestuurd. # Beslist 19-08-2026, open punt 1 van het plan Configuratie. - "50022:50022" volumes: # Beide door umbrelOS ingevuld uit een .template bij het starten. De # pagina wordt als los bestand gemount en niet als map: de bron staat in # de app-root, want alleen daar wordt hij bij een update ververst. - ${APP_DATA_DIR}/nginx.conf:/etc/nginx/nginx.conf:ro - ${APP_DATA_DIR}/index.html:/usr/share/nginx/html/index.html:ro # Het app-icoon, voor de kop van de pagina en het tabblad. Geen template, # dus dit bestand komt alleen bij een installatie mee en niet bij een # update; voor een plaatje dat vrijwel nooit wijzigt is dat goed genoeg. # De pagina valt terug op een ingebouwd merkje als de mount er niet is. - ${APP_DATA_DIR}/icon.png:/usr/share/nginx/html/icon.png:ro # Het stream-blok, dus de TLS-poort zelf. Het staat los van nginx.conf # omdat nginx niet start met een 'listen ssl' zonder certificaat, en het # command-blok hieronder zet het pas in /var/lib/gate/tls/ zodra de agent # een certificaat gekozen heeft. Tot die tijd draait alleen de pagina. - ${APP_DATA_DIR}/stream.conf:/etc/nginx/stream.conf:ro # Dezelfde gedeelde toestand als de agent. nginx leest hier cert.conf en # status.json, en schrijft de sessielog. - ${APP_DATA_DIR}/data/runtime:/var/lib/gate # De certificaten zelf moeten ook hier gemount zijn: de agent bepaalt # welk pad in cert.conf komt, maar nginx moet het bestand kunnen openen. - ${UMBREL_ROOT}/app-data/zoraxy/data/config/conf/certs:/certs/zoraxy:ro - ${APP_DATA_DIR}/data/certs:/certs/own:ro command: - /bin/sh - -c - | set -eu mkdir -p /var/lib/gate # Het log_format van de stream-sessies wordt hier geschreven en niet in # nginx.conf.template. Een log_format bestaat uit nginx-variabelen met # een dollarteken, en de template-invulling van umbreld zou die # stilzwijgend leegmaken. In dit blok is een dollarteken als $$ te # ontsnappen, dus hier kan het wel. # # Let op wat er NIET in staat: geen client-adres en geen bronpoort. Dat # is precies het soort gegeven dat deze app van het netwerk af houdt, en # om te zien dat het werkt is het niet nodig. cat > /var/lib/gate/stream-log.conf <<'CONF' log_format gate '$$time_iso8601 $$status $$bytes_received $$bytes_sent $$session_time'; access_log /var/lib/gate/stream.log gate; CONF # De TLS-poort aan of uit zetten, naar de toestand van cert.conf. # # Hier zat tot 0.0.3 een lus die wachtte tot de agent een certificaat # gekozen had, en dat was fout: nginx startte dan niet, dus de pagina # kwam niet omhoog, en de pagina is juist waar je dat certificaat kiest. # Zonder Zoraxy, of met twee kandidaten, hing de app daarmee vast op een # keuze die nergens te maken was. # # Nu start nginx altijd. Het stream-blok komt erbij zodra cert.conf er # is, en verdwijnt weer als de agent zijn keuze intrekt. nginx.conf haalt # deze map op met een jokerteken; die matcht dan niets en dat is geen # fout. mkdir -p /var/lib/gate/tls # # Deze functie eindigt altijd geslaagd, en dat is met opzet: het script # draait onder 'set -e', dus een mislukte cp zou de aanroeper afbreken. # In de lus hieronder is die aanroeper de wachtlus, en die mag om geen # enkele reden stoppen. sync_tls() { if [ -f /var/lib/gate/cert.conf ]; then # Kopieren en niet linken: nginx opent dit pad als de gebruiker # nginx, en een symlink naar een read-only mount is nodeloos fragiel. if cp /etc/nginx/stream.conf /var/lib/gate/tls/stream.conf; then echo "Certificaat aanwezig, TLS luistert op 50022." else echo "stream.conf kon niet worden weggezet; TLS blijft uit." fi else rm -f /var/lib/gate/tls/stream.conf echo "Nog geen certificaat gekozen; TLS staat uit, de pagina werkt." fi } sync_tls # De verbindingsteller. nginx stream schrijft zijn logregel pas bij het # sluiten van een sessie, en een wallet houdt zijn verbinding uren open; # zonder deze teller lijkt een actieve wallet dus afwezig. Dat was de # vraag van de gebruiker op 20-08-2026, en het open punt uit ยง4e van het # plan Webinterface: hoe die toestand binnen de container af te lezen is. # # Antwoord: /proc/net/tcp. Dat geldt per netwerk-namespace, dus dit ziet # alleen de sockets van deze container, en daarom staat deze teller hier # en niet in de agent: die zit in een andere namespace en kan er niet bij. # Kolom 2 is het lokale adres met de poort in hex, kolom 4 de toestand. # C366 is 50022 en 01 is ESTABLISHED, dus de luisterende socket (0A) en de # verbindingen naar de backend en naar de pagina vallen er buiten. # # Geteld wordt alleen het aantal. Geen adres en geen bronpoort, net als in # het log_format hierboven. count_sessions() { cat /proc/net/tcp /proc/net/tcp6 2>/dev/null \ | awk '$$4 == "01" && $$2 ~ /:C366$$/ { n++ } END { print n+0 }' } # Meteen een 0 neerzetten, zodat de pagina "geen verbindingen" kan tonen # in plaats van "onbekend" in het minuutje voor de eerste ronde. count_sessions > /var/lib/gate/sessions # De herlaadlus. De agent kan nginx niet zelf herladen: dat zou de # Docker-socket vragen en die is er bewust uit. In plaats daarvan zet hij # een vlagbestand neer en herlaadt nginx zichzelf. Een reload leest het # nieuwe certificaat in zonder bestaande verbindingen te verbreken. # # Deze lus draait op de achtergrond en nginx wordt hieronder met exec het # hoofdproces. Andersom gaat twee keer mis: de shell blijft dan PID 1 en # geeft signalen niet door, en als nginx omvalt eindigt het script met # exitcode 0, waardoor Docker een geslaagde afsluiting ziet en # 'restart: on-failure' niet ingrijpt. ( while true; do sleep 10 # Eerst schrijven, dan mv: de agent leest dit bestand op zijn eigen # moment en mag geen half bestand zien. count_sessions > /var/lib/gate/sessions.tmp mv /var/lib/gate/sessions.tmp /var/lib/gate/sessions if [ -f /var/lib/gate/reload ]; then echo "Certificaat gewijzigd, nginx herladen ..." # Eerst wegzetten, dan herladen. Andersom zou een herlading die # mislukt de vlag toch opruimen, en dan probeert hij het nooit meer. mv /var/lib/gate/reload /var/lib/gate/reload.done # Voor de herlading, niet erna: een reload leest de configuratie # opnieuw, dus het stream-blok moet er dan al liggen. sync_tls # De herlading mag niet met 'set -e' meegaan. Een certificaat dat # nginx niet aanneemt laat de reload falen, en dan zou deze lus # verdwijnen: geen enkele latere wijziging wordt dan nog opgepikt, # zonder dat er iets te zien is. if ! nginx -s reload; then echo "Herladen mislukt; nginx houdt de vorige configuratie." fi elif [ -f /var/lib/gate/cert.conf ] && [ ! -f /var/lib/gate/tls/stream.conf ]; then # De vangnetregel. De agent zet de vlag bij elke wijziging, dus # normaal komt hier niets langs; wel als de vlag verloren gaat of # als cert.conf van een vorige installatie al klaarlag. Zonder deze # tak zou TLS dan uit blijven staan tot de volgende wijziging. echo "Certificaat gevonden zonder herlaadvlag, TLS aanzetten ..." sync_tls if ! nginx -s reload; then echo "Herladen mislukt; nginx houdt de vorige configuratie." fi fi done ) & exec nginx -g 'daemon off;'