Merge pull request 'Fix wolf-pair: scan for a free port instead of hardcoding 8090' (#1) from claude/wolf-pair-port-conflict-7nz8qg into main

Reviewed-on: https://git.theaudcouple.net/root/ubuntu-post-install/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
2026-08-31 17:48:38 +00:00
+52 -3
View File
@@ -174,6 +174,19 @@ CBLOCK
[ -f "$_file" ] || return 0 [ -f "$_file" ] || return 0
cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
} }
port_in_use() {
local _port="$1" _proto="${2:-tcp}"
local _flag="-tlnH"
[ "$_proto" = "udp" ] && _flag="-ulnH"
ss "$_flag" "sport = :${_port}" 2>/dev/null | grep -q .
}
find_free_port() {
local _varname="$1" _port="$2" _proto="${3:-tcp}"
while port_in_use "$_port" "$_proto"; do
_port=$((_port + 1))
done
eval "$_varname='$_port'"
}
fi fi
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
@@ -206,11 +219,29 @@ install_wolf-pair() {
echo " - Build the wolf-pair image (python:3.12-alpine + docker-cli)" echo " - Build the wolf-pair image (python:3.12-alpine + docker-cli)"
echo " - Run the container with network_mode: host (for localhost:47989 access)" echo " - Run the container with network_mode: host (for localhost:47989 access)"
echo " - Mount /var/run/docker.sock:ro (for docker logs wolf)" echo " - Mount /var/run/docker.sock:ro (for docker logs wolf)"
echo " - Open port $WOLFPAIR_PORT in UFW" echo " - Open port $WOLFPAIR_PORT in UFW (auto-scanned for a free host port —"
echo " other services, e.g. wordpress/ntfy/beszel, default to 8090 too)"
echo " - Optionally configure a Caddy reverse proxy" echo " - Optionally configure a Caddy reverse proxy"
return 0 return 0
fi fi
# network_mode: host means there's no HOST:CONTAINER ports: mapping to scan
# around a collision on — server.py binds 0.0.0.0 directly on the host, so a
# taken 8090 (wordpress/ntfy/beszel all default here too) fails at container
# start with "address already in use" and nothing in docker-compose.yml to
# point at. Scan once and persist in .env; on a rerun, keep the port already
# in use rather than silently moving it out from under an existing Caddy
# site block / bookmarked URL.
if [ -f "$WOLFPAIR_DIR/.env" ]; then
local _existing_port
_existing_port="$(grep '^WOLFPAIR_PORT=' "$WOLFPAIR_DIR/.env" 2>/dev/null | cut -d= -f2-)"
[ -n "$_existing_port" ] && WOLFPAIR_PORT="$_existing_port"
else
find_free_port WOLFPAIR_PORT "$WOLFPAIR_PORT"
fi
[ "$WOLFPAIR_PORT" != "8090" ] && \
log_info "Port 8090 already in use — wolf-pair will use $WOLFPAIR_PORT instead."
mkdir -p "$WOLFPAIR_DIR" mkdir -p "$WOLFPAIR_DIR"
ensure_docker_dir_ownership "$WOLFPAIR_DIR" ensure_docker_dir_ownership "$WOLFPAIR_DIR"
cd "$WOLFPAIR_DIR" || return 1 cd "$WOLFPAIR_DIR" || return 1
@@ -234,10 +265,11 @@ submitted — otherwise the user resubmits a dead secret and Wolf returns
"key not found". We track submitted secrets and fall back to the waiting page "key not found". We track submitted secrets and fall back to the waiting page
until Moonlight initiates a brand-new pairing (which mints a new secret). until Moonlight initiates a brand-new pairing (which mints a new secret).
""" """
import json, subprocess, re, urllib.request, urllib.error import json, os, subprocess, re, urllib.request, urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
WOLF_HTTP = "http://localhost:47989" WOLF_HTTP = "http://localhost:47989"
LISTEN_PORT = int(os.environ.get("WOLFPAIR_PORT", "8090"))
# Secrets already submitted to Wolf. Wolf erases a secret on first submit, so a # Secrets already submitted to Wolf. Wolf erases a secret on first submit, so a
# secret in here is dead — show the waiting page instead of re-offering it. # secret in here is dead — show the waiting page instead of re-offering it.
@@ -390,7 +422,7 @@ class Handler(BaseHTTPRequestHandler):
if __name__ == '__main__': if __name__ == '__main__':
HTTPServer.allow_reuse_address = True HTTPServer.allow_reuse_address = True
HTTPServer(('0.0.0.0', 8090), Handler).serve_forever() HTTPServer(('0.0.0.0', LISTEN_PORT), Handler).serve_forever()
PYEOF PYEOF
log_success "server.py written" log_success "server.py written"
@@ -420,12 +452,27 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: wolf-pair container_name: wolf-pair
network_mode: host network_mode: host
environment:
- WOLFPAIR_PORT=${WOLFPAIR_PORT:-8090}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped restart: unless-stopped
COMPOSE COMPOSE
log_success "docker-compose.yml written" log_success "docker-compose.yml written"
# host networking means server.py binds this port directly — .env feeds it
# to the container's WOLFPAIR_PORT (above) via docker compose's own .env
# auto-load, same pattern as WOLF_STATE_DIR in services/wolf.sh.
backup_if_exists "$WOLFPAIR_DIR/.env"
cat > "$WOLFPAIR_DIR/.env" << EOF
# Port wolf-pair's pairing UI listens on (host networking — no port mapping
# to edit). Auto-scanned at install time to avoid clashing with other
# services that also default to 8090 (wordpress, ntfy, beszel).
WOLFPAIR_PORT=${WOLFPAIR_PORT}
EOF
chmod 600 "$WOLFPAIR_DIR/.env"
chown "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR/.env"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR"
# ── 4. Caddy (optional) ─────────────────────────────────────────────────── # ── 4. Caddy (optional) ───────────────────────────────────────────────────
@@ -491,6 +538,8 @@ docker compose logs -f # follow logs
- If you set up a Caddy subdomain (e.g. `wolf-pair.yourdomain.com`), that - If you set up a Caddy subdomain (e.g. `wolf-pair.yourdomain.com`), that
subdomain is for the PIN form only. subdomain is for the PIN form only.
MD MD
[ "$WOLFPAIR_PORT" != "8090" ] && \
sed -i "s/localhost:8090/localhost:${WOLFPAIR_PORT}/g" "$WOLFPAIR_DIR/README.md"
# ── 7. Build & start ────────────────────────────────────────────────────── # ── 7. Build & start ──────────────────────────────────────────────────────
echo "" echo ""