488 lines
23 KiB
Markdown
488 lines
23 KiB
Markdown
# appServer Portal UI
|
||
|
||
Übersichts-Webserver für **`server.schooltech.ch`**. Er fasst eine Reihe von
|
||
Web-Diensten (Roboter-Steuerungen, Guacamole-Remotedesktops, Portainer,
|
||
VS Code, Nextcloud …) unter **einer Domain** zusammen und macht sie über
|
||
sprechende Subdomains erreichbar.
|
||
|
||
Alle Dienste laufen als Subdomain von `.server.schooltech.ch` – egal ob sie
|
||
auf dem Server selbst, auf einem Gerät im LAN oder hinter einem SSH-Tunnel
|
||
stehen. Ein nginx-Reverse-Proxy nimmt jede Subdomain auf 443 entgegen,
|
||
terminiert TLS (Let's Encrypt) und leitet an den passenden Upstream weiter.
|
||
|
||
---
|
||
|
||
## Inhalt
|
||
|
||
- [Architektur-Übersicht](#architektur-übersicht)
|
||
- [Die Portal-Seite (`server.schooltech.ch`)](#die-portal-seite-serverschooltechch)
|
||
- [Die Dienste / Subdomains](#die-dienste--subdomains)
|
||
- [`forwarding.conf` – das Herzstück](#forwardingconf--das-herzstück)
|
||
- [`connect-proxies.sh` – Konfig-Generator](#connect-proxiessh--konfig-generator)
|
||
- [Authentifizierung](#authentifizierung)
|
||
- [TLS / Let's Encrypt](#tls--lets-encrypt)
|
||
- [Container & Betrieb](#container--betrieb)
|
||
- [Eine neue Seite ins Portal aufnehmen](#eine-neue-seite-ins-portal-aufnehmen)
|
||
- [Dateiübersicht](#dateiübersicht)
|
||
|
||
---
|
||
|
||
## Architektur-Übersicht
|
||
|
||
Alles hängt unter `*.server.schooltech.ch`. Der Unterschied zwischen den
|
||
Diensten ist nicht die Domain, sondern **wohin der Upstream zeigt**:
|
||
|
||
<img src="doc/Architektur.png" width="900" alt="Architektur-Übersicht: Internet → nginx Reverse-Proxy (appServer_PortalUI) → Subdomains, gruppiert nach Upstream-Ziel">
|
||
|
||
> Bild: [`doc/Architektur.png`](doc/Architektur.png) · Vektor-Quelle:
|
||
> [`doc/Architektur.svg`](doc/Architektur.svg) (für PDF/beliebige Skalierung).
|
||
|
||
<details>
|
||
<summary>Dieselbe Übersicht als ASCII-Text</summary>
|
||
|
||
```text
|
||
Internet (HTTPS :443 / HTTP :80)
|
||
│
|
||
┌────────────────────────────────────────────────┐
|
||
│ nginx Reverse-Proxy (Container: appServer_PortalUI)
|
||
│ ein vHost pro Subdomain *.server.schooltech.ch
|
||
│ TLS-Terminierung (Let's Encrypt) · 80→443 Redirect
|
||
└────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────┬───────────┼──────────────┬────────────────────┐
|
||
▼ ▼ ▼ ▼
|
||
server. rp5*. nextcloud. inf*/rp3*/tc*/robot*/fluidnc*
|
||
schooltech.ch schooltech. schooltech. .server.schooltech.ch
|
||
Portal-UI lokale Gerät im LAN über SSH-Tunnel-Hub
|
||
(diese App) Container (direkte IP) appServer_TunnelHead
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
public/ appServer_ 192.168.0.210 appServer_TunnelHead (SSH-Reverse-Tunnels)
|
||
index.html guacamole / ├─ 99xx InformatikWeb (inf*)
|
||
+ Auth-API portainer ├─ 81xx RP3/SCARA (rp3*, fluidnc*)
|
||
└─ 97xx ThinkCentre (tc*, robot*)
|
||
```
|
||
|
||
</details>
|
||
|
||
**Bausteine (Docker-Container, siehe `docker-compose.yaml`):**
|
||
|
||
| Container | Image | Rolle |
|
||
|---|---|---|
|
||
| `appServer_PortalUI` | `nginx:alpine` | Reverse-Proxy + Auslieferung der Portal-Seite (Ports 80/443) |
|
||
| `AppServerAuth` | `node:24-alpine` | Login-/Session-Service (`auth/auth.js`, Port 3000 intern) |
|
||
| `appServer_TunnelHead` | `linuxserver/openssh-server` | SSH-Hub: entfernte Geräte bauen Reverse-Tunnel hierher auf |
|
||
| `appServer_guacamole` | `abesnier/guacamole` | Lokaler Guacamole-Remotedesktop |
|
||
| `appServer_LetsEncryptFetcher` | `certbot/certbot` | Holt/erneuert die TLS-Zertifikate |
|
||
|
||
Alle Container hängen im Docker-Netzwerk **`appRobotNet`** und sprechen sich
|
||
per Container-Namen an (z. B. `appserverauth:3000`).
|
||
|
||
---
|
||
|
||
## Die Portal-Seite (`server.schooltech.ch`)
|
||
|
||
Ruft man die nackte Domain `server.schooltech.ch` auf, erscheint die
|
||
**Portal-Oberfläche** – eine schlanke Single-Page-App aus `public/`:
|
||
|
||
- **`public/index.html`** – Grundgerüst: oben eine **Navigationsleiste**
|
||
(`<header>` mit Logo *„schooltech“*, der Service-Navigation `#services`
|
||
und einem Login/Logout-Button), darunter ein **`<iframe>`**, in dem der
|
||
gewählte Dienst eingeblendet wird.
|
||
- **`public/app.js`** – die Logik:
|
||
- hält eine Liste der verlinkten Dienste (`services`-Array),
|
||
- prüft beim Laden via `GET /api/status`, ob eine Session besteht,
|
||
- blendet **nach dem Login** für jeden Dienst einen **Button in die
|
||
Navigationsleiste** ein,
|
||
- öffnet beim Klick den Dienst im `<iframe>` (und meldet das Ereignis an
|
||
`POST /api/event`),
|
||
- Login/Logout laufen über `POST /api/login` bzw. `POST /api/logout`.
|
||
- **`public/style.css`** – Styling der Leiste, der Buttons und des
|
||
Login-Dialogs.
|
||
|
||
**Ablauf aus Nutzersicht:**
|
||
|
||
<img src="doc/Portal.png" width="720" alt="Portal-Ansicht: Navigationsleiste oben mit Logo und Dienst-Buttons, darunter der gewählte Dienst im iFrame">
|
||
|
||
*Oben die Navigationsleiste mit Logo und Dienst-Buttons, darunter der gewählte
|
||
Dienst im iFrame. Bild: [`doc/Portal.png`](doc/Portal.png) · Quellen:
|
||
[`doc/Portal.svg`](doc/Portal.svg), [`doc/Portal.pdf`](doc/Portal.pdf).*
|
||
|
||
<details>
|
||
<summary>Layout-Skizze als ASCII-Text</summary>
|
||
|
||
```text
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ schooltech [ Control GamePad ][ Guacamole ][ Simulation ]… [Logout] │ ← Navigationsleiste
|
||
├──────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ « ausgewählter Dienst im iFrame » │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
</details>
|
||
|
||
1. Seite öffnen → ist man nicht eingeloggt, zeigt der Button **„Login“**.
|
||
2. Login (User/Passwort) → der Auth-Service setzt ein Session-Cookie für die
|
||
gesamte Domain `.server.schooltech.ch`.
|
||
3. Die Navigationsleiste füllt sich mit den Dienst-Buttons.
|
||
4. Klick auf einen Button blendet den Dienst im iFrame ein.
|
||
|
||
> **Hinweis:** Die im Portal angezeigten Buttons stehen **hardcodiert** in
|
||
> `public/app.js` (`services`-Array, aktuell 10 Einträge) – sie werden *nicht*
|
||
> automatisch aus `forwarding.conf` erzeugt. Ein neuer Dienst in
|
||
> `forwarding.conf` taucht also erst dann in der Navigationsleiste auf, wenn er
|
||
> auch ins `services`-Array eingetragen wird (siehe
|
||
> [Eine neue Seite ins Portal aufnehmen](#eine-neue-seite-ins-portal-aufnehmen)).
|
||
|
||
Aktuell im Portal verlinkte Dienste (`public/app.js`):
|
||
|
||
| Button | Ziel-Subdomain |
|
||
|---|---|
|
||
| Control GamePad | `tccontrol.server.schooltech.ch` |
|
||
| Guacamole | `rp5guac.server.schooltech.ch` |
|
||
| Simulation | `tcSimulation.server.schooltech.ch` |
|
||
| Video | `robotVideo.server.schooltech.ch` |
|
||
| Homing | `robotHoming.server.schooltech.ch` |
|
||
| RobotBase | `robotBase.server.schooltech.ch` |
|
||
| RobotEllbow | `robotEllbow.server.schooltech.ch` |
|
||
| RobotHand | `robotHand.server.schooltech.ch` |
|
||
| RobotDriver | `robotDriver.server.schooltech.ch` |
|
||
| VSCode | `robotVSCode.server.schooltech.ch` |
|
||
|
||
---
|
||
|
||
## Die Dienste / Subdomains
|
||
|
||
Quelle der Wahrheit ist `forwarding.conf`. Aus jeder Zeile generiert
|
||
`connect-proxies.sh` einen nginx-vHost. Gruppiert nach Upstream-Ziel:
|
||
|
||
### Portal selbst
|
||
|
||
| Subdomain | Upstream | Bemerkung |
|
||
|---|---|---|
|
||
| `server.schooltech.ch` | `local` | liefert die Portal-Seite aus `public/` |
|
||
|
||
### Lokale Container (auf dem Server / „RP5“)
|
||
|
||
| Subdomain | Upstream | WebSocket |
|
||
|---|---|---|
|
||
| `rp5Guac.server.schooltech.ch` | `http://appServer_guacamole:8080` | ✓ |
|
||
| `rp5Portainer.server.schooltech.ch` | `http://portainer:9000` | ✓ |
|
||
|
||
### Gerät im LAN (direkte IP)
|
||
|
||
| Subdomain | Upstream | WebSocket |
|
||
|---|---|---|
|
||
| `nextcloud.server.schooltech.ch` | `http://192.168.0.210:9183` | ✓ |
|
||
|
||
### Über `appServer_TunnelHead` (SSH-Tunnel-Hub)
|
||
|
||
Entfernte Geräte bauen einen SSH-Reverse-Tunnel zum `appServer_TunnelHead`
|
||
auf; deren Web-Oberflächen erscheinen dort auf festen Ports.
|
||
|
||
**InformatikWeb (99xx):**
|
||
|
||
| Subdomain | Upstream | WS |
|
||
|---|---|---|
|
||
| `infPortainer.server.schooltech.ch` | `http://appServer_TunnelHead:9903` | ✓ |
|
||
| `infGuac.server.schooltech.ch` | `http://appServer_TunnelHead:9980` | ✓ |
|
||
|
||
**RP3 – Raspi für die SCARA-Roboter (81xx):**
|
||
|
||
| Subdomain | Upstream | WS |
|
||
|---|---|---|
|
||
| `rp3Portainer.server.schooltech.ch` | `http://appServer_TunnelHead:8100` | ✓ |
|
||
| `rp3Guac.server.schooltech.ch` | `http://appServer_TunnelHead:8180` | ✓ |
|
||
| `fluidncRed.server.schooltech.ch` | `http://appServer_TunnelHead:8120` | ✓ |
|
||
| `fluidncWhite.server.schooltech.ch` | `https://appServer_TunnelHead:8104` | ✓ |
|
||
|
||
**ThinkCentre – MiniPC neben einem Roboter (97xx):**
|
||
|
||
| Subdomain | Upstream | WS | Auth |
|
||
|---|---|---|---|
|
||
| `tcGuac.server.schooltech.ch` | `http://appServer_TunnelHead:9780` | – | – |
|
||
| `tcPortainer.server.schooltech.ch` | `http://appServer_TunnelHead:9703` | ✓ | – |
|
||
| `tcSimulation.server.schooltech.ch` | `https://appServer_TunnelHead:9712` | ✓ | – |
|
||
| `tcControl.server.schooltech.ch` | `https://appServer_TunnelHead:9710` | ✓ | **✓** |
|
||
| `robotHoming.server.schooltech.ch` | `https://appServer_TunnelHead:9793` | ✓ | – |
|
||
| `robotVideo.server.schooltech.ch` | `https://appServer_TunnelHead:9743` | ✓ | **✓** |
|
||
| `robotBase.server.schooltech.ch` | `https://appServer_TunnelHead:9725` | ✓ | – |
|
||
| `robotEllbow.server.schooltech.ch` | `https://appServer_TunnelHead:9726` | ✓ | – |
|
||
| `robotHand.server.schooltech.ch` | `https://appServer_TunnelHead:9727` | ✓ | – |
|
||
| `robotDriver.server.schooltech.ch` | `https://appServer_TunnelHead:9798` | ✓ | **✓** |
|
||
| `robotVSCode.server.schooltech.ch` | `http://appServer_TunnelHead:9744` | ✓ | – |
|
||
|
||
> Die Port-Konvention am Tunnel-Hub: **81xx → RP3**, **97xx → ThinkCentre**,
|
||
> **99xx → InformatikWeb**.
|
||
|
||
---
|
||
|
||
## `forwarding.conf` – das Herzstück
|
||
|
||
Eine Zeile pro Subdomain. Spalten (durch Whitespace getrennt):
|
||
|
||
```
|
||
server_name upstream_url http_behavior websockets verify_upstream_tls [cert_domain] [listen_port] [auth_required]
|
||
```
|
||
|
||
| # | Spalte | Werte | Default | Bedeutung |
|
||
|---|---|---|---|---|
|
||
| 1 | `server_name` | FQDN | – | die Subdomain, z. B. `tcGuac.server.schooltech.ch` |
|
||
| 2 | `upstream_url` | `http(s)://host:port` oder `local` | – | Ziel; `local` = Portal-Seite statisch ausliefern |
|
||
| 3 | `http_behavior` | `redirect` / sonst | `redirect` | `redirect` erzeugt zusätzlich einen 80→443-Redirect-vHost |
|
||
| 4 | `websockets` | `true` / `false` | `false` | bei `true` werden `Upgrade`/`Connection`-Header durchgereicht |
|
||
| 5 | `verify_upstream_tls` | `true` / `false` | `false` | nur bei `https`-Upstream: Zertifikat des Upstreams prüfen |
|
||
| 6 | `cert_domain` | Ordnername | = `server_name` | Let's-Encrypt-Verzeichnis unter `/etc/letsencrypt/live/<cert_domain>/` |
|
||
| 7 | `listen_port` | Port | `443` | abweichender HTTPS-Listen-Port |
|
||
| 8 | `auth_required` | `true` / `false` | `false` | bei `true` schützt nginx den vHost via `auth_request` |
|
||
|
||
Zeilen, die mit `#` beginnen, sowie Leerzeilen werden ignoriert.
|
||
|
||
---
|
||
|
||
## `connect-proxies.sh` – Konfig-Generator
|
||
|
||
Läuft **automatisch beim Containerstart** des nginx-Containers (liegt als
|
||
`/docker-entrypoint.d/40-connect-proxies.sh`). Pro `forwarding.conf`-Zeile:
|
||
|
||
1. **Globale Map + Resolver** schreiben (`_globals.generated.conf`) – nötig für
|
||
WebSockets und für die DNS-Auflösung der Upstreams zur Laufzeit. Außerdem ein
|
||
**443-Default-Server** (`00-default-server.generated.conf`), der unbekanntes
|
||
SNI per `ssl_reject_handshake` abweist (siehe Hinweis unten).
|
||
2. **Alte generierte Configs** löschen (`*-https.generated.conf`,
|
||
`*-http-redirect.generated.conf`).
|
||
3. Pro Dienst eine vHost-Datei erzeugen – mit Fallunterscheidung:
|
||
- **`local`** → statischer Server, der `public/` ausliefert (+ `/api/`-Proxy
|
||
zum Auth-Service).
|
||
- **Reverse-Proxy** → `proxy_pass` auf den Upstream; optional WebSocket-Header,
|
||
optional `proxy_ssl_verify`, optional `auth_request`-Schutz.
|
||
- **DNS nicht auflösbar** → statt Proxy ein **503-Platzhalter** („Service
|
||
nicht erreichbar“), damit nginx trotzdem startet.
|
||
- **Kein Zertifikat vorhanden** → vHost wird **übersprungen** (keine 443-Conf);
|
||
solche Anfragen fängt der 443-Default-Server ab (siehe Hinweis unten).
|
||
- Bei `http_behavior = redirect` zusätzlich ein **80→443-Redirect** (mit
|
||
Ausnahme für die ACME-Challenge).
|
||
4. Abschließend **`nginx -t`** zur Syntaxprüfung.
|
||
|
||
So ist das System robust: fehlt ein Zertifikat oder ein Backend, fällt nur der
|
||
betroffene Dienst aus – nginx selbst läuft weiter.
|
||
|
||
> **Anti-Leak / Default-Server:** Ohne expliziten Default bedient nginx eine
|
||
> Anfrage mit unbekanntem `server_name` mit dem *ersten* 443-Block (alphabetisch –
|
||
> das war `ai.server.schooltech.ch`). Damit eine Domain ohne passenden vHost
|
||
> (z. B. fehlendes Zertifikat) **nicht still** bei einem fremden Dienst landet,
|
||
> erzeugt das Skript einen Catch-all `listen 443 ssl default_server;
|
||
> ssl_reject_handshake on;` – unbekanntes SNI bekommt einen sauberen TLS-Abbruch.
|
||
> `server.schooltech.ch` & Co. treffen ihren eigenen vHost weiterhin per SNI-Match.
|
||
|
||
> **Aktiver Pfad vs. Referenz:** Die vHosts werden zur Laufzeit von
|
||
> `connect-proxies.sh` aus `forwarding.conf` erzeugt. Die statischen Dateien in
|
||
> `nginxPages/` sind **nicht** in `docker-compose.yaml` gemountet und dienen nur
|
||
> als Referenz/Vorlage (z. B. `10-server-schooltech.conf` zeigt eine Variante
|
||
> der Portal-vHost-Konfig).
|
||
|
||
---
|
||
|
||
## Authentifizierung
|
||
|
||
Der **Auth-Service** (`auth/auth.js`, Express) verwaltet Login und Sessions:
|
||
|
||
- **`POST /api/login`** – prüft User/Passwort gegen `auth/users.json`
|
||
(bcrypt-Hashes) und setzt bei Erfolg ein Cookie **`SESSIONID`**.
|
||
- **`POST /api/logout`** – löscht die Session und das Cookie.
|
||
- **`GET /api/status`** – sagt dem Frontend, ob eine Session aktiv ist.
|
||
- **`GET /internal/auth`** – Endpoint für nginx `auth_request`: liefert `200`
|
||
bei gültiger Session, sonst `401`.
|
||
- **`POST /api/event`** – einfaches Logging der Button-Klicks.
|
||
|
||
Das Cookie wird auf **`domain: .server.schooltech.ch`** gesetzt und gilt damit
|
||
für **alle Subdomains**. Dienste mit `auth_required = true` (z. B. `tcControl`,
|
||
`robotVideo`, `robotDriver`) lässt nginx nur durch, wenn `auth_request` gegen
|
||
`/internal/auth` ein `200` zurückgibt – sonst landet der Nutzer nicht auf dem
|
||
Dienst.
|
||
|
||
> Sessions liegen **in-memory** im Auth-Service – ein Neustart des Containers
|
||
> meldet alle Nutzer ab.
|
||
>
|
||
> Benutzer/Passwörter pflegt man in `auth/users.json`; einen neuen
|
||
> bcrypt-Hash erzeugt `auth/cretePassword.js`.
|
||
|
||
---
|
||
|
||
## TLS / Let's Encrypt
|
||
|
||
- Zertifikate liegen unter `letsencrypt/conf/live/<domain>/` und werden in den
|
||
nginx-Container gemountet (`/etc/letsencrypt`).
|
||
- **`letsEncrypt.sh`** holt/erneuert per `certbot certonly --webroot` ein
|
||
Zertifikat pro Domain (über den `appServer_LetsEncryptFetcher`-Container,
|
||
HTTP-01-Challenge unter `/.well-known/acme-challenge/`).
|
||
- **`letsEncrypt_crontab.txt`** enthält den Cron-Eintrag für die automatische
|
||
Erneuerung.
|
||
|
||
---
|
||
|
||
## Container & Betrieb
|
||
|
||
Alles wird per `docker-compose.yaml` orchestriert. Wichtige Host-Pfade (auf dem
|
||
Server unter `/home/chk/Documents/appServerPortalUI/`):
|
||
|
||
| Host-Pfad | Mount im Container | Zweck |
|
||
|---|---|---|
|
||
| `nginx.conf` | `…/conf.d/default.conf` | Basis-vHost (Port 80, ACME, `/api/`, SPA) |
|
||
| `public/` | `…/nginx/html` | die Portal-Seite |
|
||
| `forwarding.conf` | `/etc/nginx/forwarding.conf` | Dienst-Definitionen |
|
||
| `connect-proxies.sh` | `/docker-entrypoint.d/40-…` | vHost-Generator |
|
||
| `letsencrypt/conf` | `/etc/letsencrypt` | Zertifikate |
|
||
| `letsencrypt/www` | `/var/www/certbot` | ACME-Webroot |
|
||
| `auth/` | `/usr/src/app` (Auth-Container) | Auth-Service-Code |
|
||
|
||
**Start / Neuladen:**
|
||
|
||
```bash
|
||
# Alles starten
|
||
docker compose up -d
|
||
|
||
# nginx-vHosts neu generieren (nach Änderung an forwarding.conf):
|
||
docker restart appServer_PortalUI
|
||
|
||
# Logs des Generators ansehen
|
||
docker logs appServer_PortalUI | grep connect-proxies
|
||
```
|
||
|
||
---
|
||
|
||
## Eine neue Seite ins Portal aufnehmen
|
||
|
||
Zwei Fragen entscheiden, wie eine Seite angebunden wird: **(1) Wie erreicht der
|
||
nginx-Container das Backend?** und **(2) unter welcher Subdomain** soll es laufen?
|
||
|
||
### Wie das Backend erreichbar ist – zwei Wege
|
||
|
||
- **A) Direkt** – das Backend ist vom nginx-Container aus erreichbar:
|
||
- **lokaler Container** im Docker-Netz `appRobotNet` → per Container-Name,
|
||
z. B. `http://open-webui:8080`.
|
||
- **Gerät im LAN** → per **IP**, z. B. `http://192.168.0.210:9183`.
|
||
- ⚠️ **Kein `.local`/mDNS!** Namen wie `thinkcentre.local` löst der Container
|
||
**nicht** auf (Docker-DNS kann kein mDNS) → der Dienst wird zum 503-Platzhalter.
|
||
Immer IP **oder** Tunnel verwenden.
|
||
- **B) Über den SSH-Reverse-Tunnel** – das Backend steht hinter NAT/Firewall
|
||
(Roboter-Pi, ThinkCentre, InformatikWeb) und hat keine Route ins Server-LAN. Es
|
||
„meldet sich“ per autossh beim Server an und wird dort auf einem festen Port
|
||
erreichbar.
|
||
|
||
### Hintergrund: der SSH-Reverse-Tunnel
|
||
|
||
Auf der **Geräteseite** (z. B. im Portainer-Stack des Roboters) läuft ein
|
||
autossh-Container `appRobot_Tunnel`, der eine Dauerverbindung zu
|
||
`tunnel@server.schooltech.ch -p 2255` (= Container `appServer_TunnelHead`)
|
||
aufbaut. Pro Dienst eine Zeile:
|
||
|
||
```
|
||
-R 0.0.0.0:<HubPort>:<container>:<port>
|
||
```
|
||
|
||
Bedeutung: „Öffne am TunnelHead den Port `<HubPort>` und leite ihn an
|
||
`<container>:<port>` auf der Geräteseite weiter.“ Damit wird `<container>:<port>`
|
||
(Geräteseite) für nginx als **`appServer_TunnelHead:<HubPort>`** erreichbar – und
|
||
`forwarding.conf` proxyt genau dorthin.
|
||
|
||
**Aktuelle Tunnel-Belegung** (aus dem autossh-Command des `appRobot_Tunnel`-Stacks):
|
||
|
||
| HubPort | → Geräteseite | Portal-Subdomain |
|
||
|---|---|---|
|
||
| 9703 | `portainer:9000` | `tcPortainer` |
|
||
| 9710 | `appRobot_Control:10010` | `tcControl` |
|
||
| 9712 | `appRobot_Simulation:1003` | `tcSimulation` |
|
||
| 9725 | `appRobot_AccessBase:443` | `robotBase` |
|
||
| 9726 | `appRobot_AccessEllbow:443` | `robotEllbow` |
|
||
| 9727 | `appRobot_AccessHand:443` | `robotHand` |
|
||
| 9743 | `AppRobotWebcam:8444` | `robotVideo` |
|
||
| 9744 | `appRobot_CodeServer:8443` | `robotVSCode` |
|
||
| 9780 | `appRobot_guacamole:8080` | `tcGuac` |
|
||
| 9793 | `appRobot_Homing:2093` | `robotHoming` |
|
||
| 9798 | `appRobot_Driver:2098` | `robotDriver` |
|
||
|
||
> Konvention der HubPorts: **81xx → RP3/SCARA**, **97xx → ThinkCentre/Roboter**,
|
||
> **99xx → InformatikWeb**. Das `0.0.0.0:` setzt voraus, dass am TunnelHead-sshd
|
||
> `GatewayPorts clientspecified` aktiv ist (ist es).
|
||
|
||
### Rezept A: neue Seite **über den Tunnel**
|
||
|
||
1. **Tunnel öffnen** – geräteseitig im autossh-Command eine `-R`-Zeile ergänzen
|
||
(freien HubPort wählen), z. B.:
|
||
```
|
||
-R 0.0.0.0:9750:appRobot_NeuerDienst:8080 \
|
||
```
|
||
Stack neu deployen (Portainer). Der Dienst ist nun `appServer_TunnelHead:9750`.
|
||
2. **`forwarding.conf`** (Server) – Zeile ergänzen; das Schema muss zu dem passen,
|
||
was das Backend spricht:
|
||
```
|
||
neuerdienst.server.schooltech.ch http://appServer_TunnelHead:9750 redirect true false
|
||
```
|
||
3. **Zertifikat** – Domain in `letsEncrypt.sh` aufnehmen (**mit `--cert-name`!**)
|
||
und ausführen; ohne Zertifikat überspringt `connect-proxies.sh` den vHost.
|
||
4. **nginx neu starten:** `docker restart appServer_PortalUI`, dann im Log prüfen:
|
||
`[+] … Zertifikat OK … Erzeuge 443`.
|
||
5. **Optional – im Portal-Menü zeigen:** Eintrag im `services`-Array in
|
||
`public/app.js`:
|
||
```js
|
||
{ id: "neu", name: "Neuer Dienst", url: "https://neuerdienst.server.schooltech.ch/" }
|
||
```
|
||
6. **Optional – Login erzwingen:** in `forwarding.conf` 8. Spalte `… 443 true`.
|
||
|
||
### Rezept B: neue Seite **direkt** (lokaler Container / LAN-IP)
|
||
|
||
Wie A, nur **Schritt 1 entfällt**. Der Upstream in `forwarding.conf` zeigt direkt
|
||
auf den Container-Namen oder die LAN-IP (niemals `.local`):
|
||
```
|
||
neuerdienst.server.schooltech.ch http://192.168.0.50:8080 redirect true false
|
||
```
|
||
(Rest identisch: Zertifikat → Restart → optional `app.js`/Auth.)
|
||
|
||
### Eine Seite entfernen
|
||
|
||
1. Zeile in `forwarding.conf` löschen (oder mit `#` auskommentieren) und ggf. den
|
||
`app.js`-Eintrag entfernen → `docker restart appServer_PortalUI`.
|
||
2. Falls über Tunnel: die `-R`-Zeile aus dem `appRobot_Tunnel`-Stack entfernen und
|
||
neu deployen.
|
||
3. Optional Zertifikat aufräumen: `certbot delete --cert-name <domain>`.
|
||
|
||
> **Spalten-Kurzreferenz** der `forwarding.conf`:
|
||
> `server_name upstream_url http_behavior websockets verify_upstream_tls [cert_domain] [listen_port] [auth_required]`
|
||
> — Details siehe [`forwarding.conf` – das Herzstück](#forwardingconf--das-herzstück).
|
||
|
||
---
|
||
|
||
## Dateiübersicht
|
||
|
||
```
|
||
appServerPortalUI/
|
||
├── docker-compose.yaml # Orchestrierung aller Container
|
||
├── nginx.conf # Basis-vHost (Port 80, ACME, /api/, SPA)
|
||
├── forwarding.conf # ← Dienst-Definitionen (Quelle der Wahrheit)
|
||
├── connect-proxies.sh # generiert die nginx-vHosts beim Start
|
||
├── letsEncrypt.sh # Zertifikate holen/erneuern
|
||
├── letsEncrypt_crontab.txt # Cron-Eintrag für Auto-Renewal
|
||
├── public/ # die Portal-Seite (server.schooltech.ch)
|
||
│ ├── index.html # Grundgerüst + Navigationsleiste
|
||
│ ├── app.js # Logik: Login, Dienst-Buttons, iFrame
|
||
│ └── style.css # Styling
|
||
├── auth/ # Auth-Service (Node/Express)
|
||
│ ├── auth.js # Login/Logout/Status/internal-auth
|
||
│ ├── users.json # Benutzer + bcrypt-Hashes
|
||
│ ├── cretePassword.js # Hilfsskript: neuen Hash erzeugen
|
||
│ └── package.json
|
||
├── letsencrypt/ # Zertifikate + ACME-Webroot
|
||
├── certs/ # (selbstsignierte) Zertifikate
|
||
├── nginxPages/ # Referenz-vHosts (NICHT gemountet)
|
||
└── doc/ # Doku, Diagramme & Notizen
|
||
├── Architektur.svg # Architektur-Diagramm (Quelle für PDF/PNG)
|
||
├── Portal.svg / Portal.pdf # Ansicht der Portal-Seite
|
||
└── AI_Gen.* , *_q?_*.txt # weitere Notizen / generierte Doku
|
||
```
|
||
|
||
> `forwarding_running_6_6_2026.conf` ist ein Snapshot der aktuell laufenden
|
||
> Konfiguration und wird hier bewusst (noch) nicht behandelt.
|