19. September 2026

RustDesk Web-Client hinter dem Nginx Proxy Manager: Fernwartung im Browser

rd RustDesk Web-Client hinter dem Nginx Proxy Manager: Fernwartung im Browser

Mit einem selbst gehosteten RustDesk-Server kannst du Rechner auch ohne installierte App fernwarten, denn der Web-Client läuft direkt im Browser. Sobald er hinter einem Reverse Proxy mit HTTPS laufen soll, gibt es aber ein paar Stolperfallen, die in der Doku kaum stehen. Dieser Beitrag zeigt das Setup mit dem Nginx Proxy Manager (NPM) und den typischen Fehlern.

Alle Domains und IPs sind Beispiele. Ersetze example.org und die 172.16.0.x-Adressen durch deine eigenen.

Was am Ende läuft

DomainZielZweck
fernwartung.example.orgWeb-Client, Port 5000Weboberfläche
api.fernwartung.example.orgrustdesk-api, Port 21114API und Admin-Oberfläche
ws-id.fernwartung.example.orghbbs, Port 21118WebSocket zum ID-Server
ws-relay.fernwartung.example.orghbbr, Port 21119WebSocket zum Relay

Der Server (hbbs/hbbr) läuft bei mir als Container unter 172.16.0.49, die API unter 172.16.0.50, der Web-Client unter 172.16.0.51.

Warum vier Domains?

Der Web-Client baut die Verbindung zum ID-Server als wss://<domain>:21116 auf. Auf 21116 lauscht hbbs aber mit seinem rohen TCP-Dienst, ohne TLS und ohne WebSocket. Der Browser bekommt keine Verbindung, und in der Konsole erscheinen Folgefehler wie curConn is undefined. Die eigentlichen WebSocket-Ports sind 21118 (hbbs) und 21119 (hbbr), sie sprechen aber unverschlüsseltes WebSocket, während die Seite über HTTPS läuft. Deshalb terminiert der NPM das TLS und bekommt dafür zwei eigene Subdomains.

Schritt 1: DNS

Lege für alle vier Domains A-Records auf die öffentliche IP des NPM-Hosts an und prüfe sie:

bash

dig +short ws-id.fernwartung.example.org

Erst wenn jede Domain die richtige IP liefert, fordere Zertifikate an, sonst scheitert die Let’s-Encrypt-Challenge.

Schritt 2: Web-Client per Docker Compose

Ich nutze das Image pmietlicki/rustdesk-web-client:v1.

yaml

  rustdesk-webclient:
    container_name: rustdesk-webclient
    image: pmietlicki/rustdesk-web-client:v1
    restart: unless-stopped
    environment:
      - CUSTOM_RENDEZVOUS_SERVER=ws-id.fernwartung.example.org:443
      - RELAY_SERVER=ws-relay.fernwartung.example.org:443
      - KEY=<OEFFENTLICHER_SCHLUESSEL>
      - API_SERVER=https://api.fernwartung.example.org
    ports:
      - 5000:5000
    networks:
      docknet:
        ipv4_address: 172.16.0.51

Drei Details entscheiden, ob es klappt:

  • Das Image :v1 lauscht intern auf Port 5000, nicht auf 80. Mit 5000:80 läuft der Container, aber die Seite bleibt leer.
  • Die Variablen heißen CUSTOM_RENDEZVOUS_SERVER, RELAY_SERVER, KEY und API_SERVER. RUSTDESK_KEY gehört zu einem anderen Image und wird ignoriert.
  • KEY ist der öffentliche Schlüssel aus der Datei id_ed25519.pub im Datenverzeichnis von hbbs.

Testen, bevor es an den NPM geht:

bash

docker compose up -d --force-recreate rustdesk-webclient
curl -I http://127.0.0.1:5000/    # erwartet: 200

Schritt 3: Proxy Hosts im NPM

Lege für jede Domain einen Proxy Host an:

DomainSchemeZielPort
fernwartung.example.orghttp172.16.0.515000
api.fernwartung.example.orghttp172.16.0.5021114
ws-id.fernwartung.example.orghttp172.16.0.4921118
ws-relay.fernwartung.example.orghttp172.16.0.4921119

Bei allen: Websockets Support an, neues Let’s-Encrypt-Zertifikat, Force SSL und HTTP/2 an. HSTS schaltest du erst nach erfolgreichem Test ein. Bei den beiden WebSocket-Hosts kommt im Zahnrad-Tab noch dies hinein, damit ruhende Sitzungen nicht nach 60 Sekunden getrennt werden:

nginx

proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

Schritt 4: Testen

bash

curl -I https://fernwartung.example.org        # 200
curl -I http://fernwartung.example.org         # 301 auf https

curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  https://ws-id.fernwartung.example.org/       # 101 Switching Protocols

Öffne den Web-Client danach in einem privaten Fenster. Er legt seine Einstellungen im localStorage ab und lädt per Service Worker, sonst hängen alte Werte fest.

Schritt 5: Die Ports 21118 und 21119 schließen

Läuft alles über 443, müssen 21118 und 21119 nicht mehr öffentlich erreichbar sein. Die RustDesk-Doku empfiehlt, sie so einzuschränken, dass nur der Reverse Proxy zugreifen kann. Vorher sicherst du die Firewall-Konfiguration:

bash

ufw status numbered > ~/ufw-status-$(date +%F).txt
tar czf ~/ufw-backup-$(date +%F).tar.gz /etc/ufw /etc/default/ufw

Dann entfernst du die Freigaben für die beiden Ports (bei mir Routing-Regeln zu 172.16.0.49) und lässt 21115 bis 21117 sowie 21116/udp bestehen. Zusätzlich löschst du in der Compose des RustDesk-Servers die beiden ports:-Zeilen für 21118 und 21119. Der NPM erreicht die Ports auch ohne Veröffentlichung über das Docker-Netzwerk. Zum Schluss testest du von außerhalb deines Netzes (ein Test vom Server selbst sagt nichts, weil er die Firewall umgeht):

bash

nc -zv -w 5 <öffentliche-IP> 21118    # Timeout erwartet
nc -zv -w 5 <öffentliche-IP> 21117    # Erfolg erwartet

Typische Fehler

  • 301 auf die Hauptdomain oder SSL_ERROR_UNRECOGNIZED_NAME_ALERT: Der NPM, der auf 80/443 antwortet, kennt die Domain nicht. Der Proxy Host fehlt, liegt in einer anderen NPM-Instanz oder ist inaktiv. Auf dem Host hilft docker exec <npm> grep -rl <domain> /config/nginx/proxy_host/.
  • NPM-Log mit Permission Denied und Cannot read properties of null (reading 'id'): Die Sitzung der Weboberfläche ist ungültig, und der Proxy Host wurde nicht gespeichert. Abmelden, neu anmelden, Host neu anlegen.
  • Could not resolve host: Der DNS-Eintrag fehlt noch.
  • HTTP liefert 200 statt eines Redirects: Force SSL ist nicht aktiv.

Offener Punkt

Bei mir lief der Web-Client danach, den Login mit Benutzername und Passwort gegen die API habe ich darin nicht zum Laufen bekommen, obwohl POST /api/login per curl 200 liefert und die Admin-Oberfläche erreichbar ist. Als Kandidaten stehen Mixed Content (API_SERVER mit https:// angeben), CORS und alte Werte im Browser im Raum. Falls du die Ursache findest, lass es mich gern wissen.

Fazit

Der Kern ist, dass der Web-Client nicht mit 21116 zurechtkommt und stattdessen WebSocket über TLS braucht. Mit zwei zusätzlichen Subdomains im NPM läuft das ohne neue öffentliche Ports, und die WebSocket-Ports lassen sich anschließend wieder schließen.