Mehrere Dienste hinter einem Port: Pfade statt Domainnamen

Hintergrund aus der Praxis: nach aussen ist nur ein Port frei, auf dem
Server laufen aber Checkmk und Grafana und noch ein Dienst. Die Frage
war, ob sich statt eines Domainnamens auch /irgendeinname nutzen laesst.

Ja - der Proxy Manager kennt dafuer "Custom locations", und die API
nimmt sie als locations-Array entgegen. Geprueft am Schema von v2.15.1
(components/proxy-host-object.json) und an der Vorlage _location.conf,
aus der nginx den location-Block erzeugt.

  proxy add --domain 10.0.0.5 --forward-port 5000 --cert self \
            --location /grafana=127.0.0.1:3000 \
            --location /sonstwas=127.0.0.1:8080

--location ist mehrfach angebbar, versteht /pfad=host:port, wahlweise
mit Schema (https://) und mit einem Pfad, der beim Ziel vorangestellt
wird (/alt=host:3000/basis). Ungueltige Angaben werden abgewiesen.
Ebenfalls geprueft: als --domain ist eine IP-Adresse zulaessig, ein
DNS-Name ist nicht noetig (das Muster im Schema laesst alles ausser
einer Handvoll Sonderzeichen zu).

Neu dazu "proxy show --domain X | --id N", das den Eintrag samt allen
Pfaden auflistet - noetig, weil --update den Eintrag vollstaendig
ersetzt und man die bestehenden Pfade wieder mitgeben muss.

Im Assistenten fragt die Oberflaeche nach dem Zertifikat, ob weitere
Dienste unter demselben Namen liegen sollen, und nimmt dann Pfad und
Ziel im Wechsel auf, bis ein Feld leer bleibt.

Dokumentiert als Abschnitt 10.3 in beiden Sprachfassungen, mit dem
Fallstrick, der sonst Stunden kostet: ein Dienst hinter einem Unterpfad
muss seinen Basispfad kennen. Fuer Grafana sind das root_url und
serve_from_sub_path; ohne die bleibt die Seite weiss, weil das
JavaScript unter /public statt /grafana/public gesucht wird. Checkmk
ist nicht betroffen, es liegt ohnehin unter /<site>/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 17:56:04 +02:00
co-authored by Claude Opus 5
parent fd96126199
commit e7eb1c3be9
3 changed files with 329 additions and 17 deletions
+129 -7
View File
@@ -180,7 +180,12 @@ KOMMANDOS - Proxy-Hosts (ueber die API des Proxy Managers)
--cert self|letsencrypt|none (Vorgabe self)
--update vorhandenen Host ersetzen
--advanced TEXT eigener nginx-Abschnitt
--location /P=HOST:PORT weiterer Pfad auf demselben Namen,
mehrfach angebbar. Damit lassen sich mehrere
Dienste hinter EINEM Port unterscheiden:
--location /grafana=127.0.0.1:3000
--no-force-ssl --no-http2 --no-websockets --no-block-exploits
proxy show --domain X | --id N Einzelheiten samt Pfaden anzeigen
proxy del --id N Proxy-Host loeschen
KOMMANDOS - Zertifikat
@@ -3182,6 +3187,36 @@ EOF
printf '%s' "$id"
}
# Einen --location-Eintrag in ein JSON-Objekt umsetzen.
# /grafana=127.0.0.1:3000
# /grafana=http://127.0.0.1:3000
# /alt=127.0.0.1:3000/basis (Pfad, der beim Ziel vorangestellt wird)
proxy_location_json() { # spec -> JSON-Objekt auf stdout
local spec="$1" path target scheme=http host port fwd=""
case "$spec" in
*=*) path="${spec%%=*}"; target="${spec#*=}" ;;
*) err "Ungueltige Angabe: '$spec'"
printf ' Erwartet: /pfad=host:port z.B. /grafana=127.0.0.1:3000\n' >&2
return 1 ;;
esac
case "$path" in /*) : ;; *) path="/$path" ;; esac
case "$target" in
http://*) scheme=http; target="${target#http://}" ;;
https://*) scheme=https; target="${target#https://}" ;;
esac
host="${target%%:*}"
local rest="${target#*:}"
if [ "$rest" = "$target" ]; then
err "Im Ziel '$target' fehlt der Port (erwartet host:port)."; return 1
fi
port="${rest%%/*}"
case "$rest" in */*) fwd="/${rest#*/}" ;; esac
valid_port "$port" || { err "'$port' ist keine gueltige Portnummer."; return 1; }
[ -n "$host" ] || { err "Im Ziel '$target' fehlt der Host."; return 1; }
printf '{"path":"%s","forward_scheme":"%s","forward_host":"%s","forward_port":%s,"forward_path":"%s","advanced_config":""}' \
"$path" "$scheme" "$host" "$port" "$fwd"
}
# --- Proxy-Hosts ---------------------------------------------------------
cmd_proxy() {
local sub="${1:-list}"; [ $# -gt 0 ] && shift || true
@@ -3234,6 +3269,39 @@ cmd_proxy() {
[ "$n" -eq 0 ] && echo "(noch kein Zertifikat vorhanden)"
return 0 ;;
show)
local pid="" dom=""
while [ $# -gt 0 ]; do
case "$1" in --id) pid="$2"; shift 2 ;; --domain) dom="$2"; shift 2 ;; *) pid="$1"; shift ;; esac
done
npm_require_running || return 1
NPM_TOKEN="$(npm_api_token)" || return 1
if [ -z "$pid" ]; then
[ -n "$dom" ] || die "--id oder --domain angeben."
local all; all="$(npm_api GET /api/nginx/proxy-hosts)"
local i
for i in $(json_ids "$all"); do
if npm_api GET "/api/nginx/proxy-hosts/$i" | grep -qF "\"$dom\""; then pid="$i"; break; fi
done
[ -n "$pid" ] || die "Kein Proxy-Host fuer '$dom' gefunden."
fi
local o; o="$(npm_api GET "/api/nginx/proxy-hosts/$pid")"
printf '\n%sProxy-Host %s%s\n' "$C_BLD" "$pid" "$C_RST"
printf ' Domains : %s\n' "$(printf '%s' "$o" | sed -n 's/.*"domain_names"[[:space:]]*:[[:space:]]*\[\([^]]*\)\].*/\1/p' | tr -d '"' || true)"
printf ' Ziel : %s://%s:%s\n' "$(json_str "$o" forward_scheme)" "$(json_str "$o" forward_host)" "$(json_num "$o" forward_port)"
printf ' Zertifikat : %s\n' "$(json_num "$o" certificate_id)"
printf '\n %sZusaetzliche Pfade%s\n' "$C_BLD" "$C_RST"
local n
n="$(printf '%s' "$o" | grep -o '"path":"[^"]*"' | wc -l || true)"
if [ "${n:-0}" -eq 0 ]; then
printf ' (keine - alles geht an das Ziel oben)\n'
else
printf '%s' "$o" | tr '}' '\n' \
| sed -n 's/.*"path":"\([^"]*\)".*"forward_host":"\([^"]*\)","forward_port":\([0-9]*\).*/ \1 -> \2:\3/p'
fi
echo
return 0 ;;
del|delete|rm)
local pid=""
while [ $# -gt 0 ]; do
@@ -3252,7 +3320,7 @@ cmd_proxy() {
local domain="${PROXY_DOMAIN:-}" fhost="${PROXY_FORWARD_HOST:-127.0.0.1}"
local fport="${PROXY_FORWARD_PORT:-5000}" scheme=http cert=self
local force_ssl=true http2=true ws=true block=true update="" adv=""
local adv_set=""
local adv_set="" locs=""
while [ $# -gt 0 ]; do
case "$1" in
--domain) domain="$2"; shift 2 ;;
@@ -3261,6 +3329,10 @@ cmd_proxy() {
--scheme) scheme="$2"; shift 2 ;;
--cert) cert="$2"; shift 2 ;;
--advanced) adv="$2"; adv_set=1; shift 2 ;;
--location) local lj
lj="$(proxy_location_json "$2")" || return 1
if [ -n "$locs" ]; then locs="$locs,$lj"; else locs="$lj"; fi
shift 2 ;;
--update) update=1; shift ;;
--no-force-ssl) force_ssl=false; shift ;;
--no-http2) http2=false; shift ;;
@@ -3298,7 +3370,7 @@ cmd_proxy() {
esac
local payload
payload="{\"domain_names\":[\"$domain\"],\"forward_scheme\":\"$scheme\",\"forward_host\":\"$fhost\",\"forward_port\":$fport,\"certificate_id\":$certid,\"ssl_forced\":$force_ssl,\"http2_support\":$http2,\"block_exploits\":$block,\"allow_websocket_upgrade\":$ws,\"caching_enabled\":false,\"advanced_config\":\"$adv\",\"enabled\":true,\"meta\":{}}"
payload="{\"domain_names\":[\"$domain\"],\"forward_scheme\":\"$scheme\",\"forward_host\":\"$fhost\",\"forward_port\":$fport,\"certificate_id\":$certid,\"ssl_forced\":$force_ssl,\"http2_support\":$http2,\"block_exploits\":$block,\"allow_websocket_upgrade\":$ws,\"caching_enabled\":false,\"advanced_config\":\"$adv\",\"locations\":[$locs],\"enabled\":true,\"meta\":{}}"
local body
if [ -n "$exid" ]; then
@@ -3316,6 +3388,10 @@ cmd_proxy() {
return 1
fi
ok "Proxy-Host $newid angelegt: $domain -> $scheme://$fhost:$fport"
if [ -n "$locs" ]; then
printf ' Zusaetzliche Pfade:\n'
printf '%s' "$locs" | tr ',' '\n' | sed -n 's/.*"path":"\([^"]*\)".*"forward_host":"\([^"]*\)","forward_port":\([0-9]*\).*/ \1 -> \2:\3/p'
fi
cat <<EOF
${C_BLD}Erreichbar${C_RST}
@@ -3329,7 +3405,7 @@ EOF
return 0 ;;
*) die "Unbekanntes Unterkommando 'proxy $sub'.
Moeglich: status | list | certs | add | del" ;;
Moeglich: status | list | show | certs | add | del" ;;
esac
}
@@ -4363,6 +4439,7 @@ Konto: ${NPM_ADMIN_EMAIL:-<nicht gesetzt>}
Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \
anlegen "Proxy-Host anlegen (Assistent)" \
list "Vorhandene Proxy-Hosts anzeigen" \
show "Einzelheiten eines Proxy-Hosts (mit Pfaden)" \
certs "Vorhandene Zertifikate anzeigen" \
del "Proxy-Host loeschen" \
status "Anmeldung pruefen" \
@@ -4371,6 +4448,11 @@ Ziel : ${PROXY_FORWARD_HOST:-127.0.0.1}:${PROXY_FORWARD_PORT:-5000}" \
anlegen) gui_proxy_add || true ;;
list) local f; f="$(mktemp)"; NO_COLOR=1 cmd_proxy list >"$f" 2>&1 || true
ui_text "Proxy-Hosts" "$f"; rm -f "$f" ;;
show) local d3; d3="$(ui_input "Einzelheiten" "Name oder ID des Proxy-Hosts:" "${PROXY_DOMAIN:-}")" || continue
[ -n "$d3" ] || continue
local f3; f3="$(mktemp)"
NO_COLOR=1 cmd_proxy show --domain "$d3" >"$f3" 2>&1 || true
ui_text "Proxy-Host $d3" "$f3"; rm -f "$f3" ;;
certs) local f2; f2="$(mktemp)"; NO_COLOR=1 cmd_proxy certs >"$f2" 2>&1 || true
ui_text "Zertifikate" "$f2"; rm -f "$f2" ;;
del) gui_proxy_del || true ;;
@@ -4426,19 +4508,59 @@ Womit soll HTTPS abgesichert werden?" \
letsencrypt "Let's Encrypt (Port 80 muss aus dem Internet erreichbar sein)" \
none "Ohne HTTPS (nur Port 80)")" || return 0
# --- weitere Dienste unter demselben Namen, nach Pfad getrennt ----
local -a locopt=()
local loclist=""
if ui_yesno "Weitere Dienste?" \
"Sollen unter demselben Namen noch andere Dienste erreichbar sein,
unterschieden nach dem Pfad?
https://$dom/ -> Checkmk (das Ziel von eben)
https://$dom/grafana -> Grafana
https://$dom/sonstwas -> ein weiterer Dienst
Das ist der Weg, wenn nur EIN Port nach aussen frei ist."; then
while :; do
local lpath ltarget
lpath="$(ui_input "Pfad" \
"Unter welchem Pfad soll der Dienst liegen?
Leer lassen beendet die Eingabe." "")" || break
[ -n "$lpath" ] || break
ltarget="$(ui_input "Ziel fuer $lpath" \
"Wohin soll $lpath weitergeleitet werden?
Schreibweise: host:port z.B. 127.0.0.1:3000
https://host:port fuer verschluesselte Ziele" "127.0.0.1:3000")" || break
[ -n "$ltarget" ] || break
if ! proxy_location_json "$lpath=$ltarget" >/dev/null 2>&1; then
ui_msg "Ungueltig" "'$lpath=$ltarget' konnte nicht gelesen werden.\n\nErwartet: /pfad und host:port"
continue
fi
locopt+=(--location "$lpath=$ltarget")
loclist="$loclist
$lpath -> $ltarget"
ui_yesno "Noch einer?" "Bisher:$loclist
Einen weiteren Pfad hinzufuegen?" || break
done
fi
local upd=""
ui_yesno "Anlegen" \
if ui_yesno "Anlegen" \
"$dom -> 127.0.0.1:$fport
Zertifikat: $cert
Zertifikat: $cert${loclist:+
Zusaetzliche Pfade:$loclist}
Einen bereits vorhandenen Eintrag fuer diesen Namen ersetzen?
<Ja> ersetzt ihn
<Nein> bricht ab, falls es ihn schon gibt" && upd=--update
<Nein> bricht ab, falls es ihn schon gibt"; then upd=--update; fi
ui_run "Proxy-Host anlegen" bash -c \
"cd '$PROJECT_DIR' && NO_COLOR=1 '$SELF' -y proxy add --domain '$dom' \
--forward-port '$fport' --cert '$cert' $upd"
--forward-port '$fport' --cert '$cert' $upd ${locopt[*]+${locopt[*]}}"
# Die Vorgaben merken, damit der naechste Aufruf sie kennt
env_set PROXY_DOMAIN "$dom" 1 2>/dev/null || true