Răsfoiți Sursa

feat(docker): support XUI_PORT runtime override (#5240)

* feat(docker): support XUI_PORT runtime override

Allow deployments to select the panel listener port without mutating the persisted webPort setting. Invalid values fall back to the database-backed port and are covered by parser boundary tests.

* docs: describe XUI_PORT deployment usage

Add commented local and Compose examples, explain runtime precedence, and call out matching Docker bridge port mappings.
Pavel 17 ore în urmă
părinte
comite
7f34c306d7
6 a modificat fișierele cu 99 adăugiri și 1 ștergeri
  1. 1 0
      .env.example
  2. 8 0
      CONTRIBUTING.md
  3. 3 1
      docker-compose.yml
  4. 18 0
      internal/config/config.go
  5. 61 0
      internal/config/config_test.go
  6. 8 0
      internal/web/web.go

+ 1 - 0
.env.example

@@ -3,3 +3,4 @@ XUI_DB_FOLDER=x-ui
 XUI_LOG_FOLDER=x-ui
 XUI_BIN_FOLDER=x-ui
 XUI_INIT_WEB_BASE_PATH=/
+# XUI_PORT=8080

+ 8 - 0
CONTRIBUTING.md

@@ -73,6 +73,7 @@ XUI_DB_FOLDER=x-ui
 XUI_LOG_FOLDER=x-ui
 XUI_BIN_FOLDER=x-ui
 XUI_INIT_WEB_BASE_PATH=/
+# XUI_PORT=8080
 ```
 
 Drop the xray binary (`xray-windows-amd64.exe` on Windows, `xray-linux-amd64` on Linux, etc.) plus the matching `geoip.dat` and `geosite.dat` files into `x-ui/`. The easiest source is a [released Xray-core build](https://github.com/XTLS/Xray-core/releases). On Windows, `wintun.dll` is also required for testing TUN inbounds.
@@ -256,9 +257,16 @@ For deeper notes on the frontend toolchain see [`frontend/README.md`](frontend/R
 | `XUI_LOG_FOLDER` | platform default | Where `3xui.log` lives |
 | `XUI_BIN_FOLDER` | `bin` | Where the xray binary, geo files, and xray `config.json` live |
 | `XUI_INIT_WEB_BASE_PATH` | `/` | The initial URI path for the web panel |
+| `XUI_PORT` | persisted `webPort` | Runtime-only web panel listener port override (`1` through `65535`) |
 | `XUI_DB_TYPE` | `sqlite` | Set to `postgres` to use PostgreSQL via `XUI_DB_DSN` |
 | `XUI_DB_DSN` | — | PostgreSQL DSN when `XUI_DB_TYPE=postgres` |
 
+A valid `XUI_PORT` takes precedence over the database-backed `webPort` for the
+current process without changing the stored setting. Unset, empty, whitespace-only,
+malformed, or out-of-range values fall back to `webPort`; invalid configured values
+also produce a warning. With Docker bridge networking, the published container port
+must match the override, for example `XUI_PORT: "8080"` with `ports: ["8080:8080"]`.
+
 ## Issues
 
 - Bug reports and feature requests: [GitHub Issues](https://github.com/MHSanaei/3x-ui/issues)

+ 3 - 1
docker-compose.yml

@@ -19,6 +19,7 @@ services:
       XRAY_VMESS_AEAD_FORCED: "false"
       XUI_ENABLE_FAIL2BAN: "true"
       # XUI_INIT_WEB_BASE_PATH: "/"
+      # XUI_PORT: "8080"
       # To use PostgreSQL instead of the default SQLite, run:
       #   docker compose --profile postgres up -d
       # and uncomment the two lines below.
@@ -26,6 +27,7 @@ services:
       # XUI_DB_DSN: "postgres://xui:xui@postgres:5432/xui?sslmode=disable"
     tty: true
     ports:
+      # When XUI_PORT is set, publish the same container port (for example "8080:8080").
       - "2053:2053"
     restart: unless-stopped
 
@@ -39,4 +41,4 @@ services:
       POSTGRES_DB: xui
     volumes:
       - $PWD/pgdata/:/var/lib/postgresql/data
-    restart: unless-stopped
+    restart: unless-stopped

+ 18 - 0
internal/config/config.go

@@ -9,6 +9,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strconv"
 	"strings"
 	"testing"
 )
@@ -63,6 +64,23 @@ func IsSkipHSTS() bool {
 	return os.Getenv("XUI_SKIP_HSTS") == "true"
 }
 
+func GetPortOverride() (port int, configured bool, err error) {
+	value, ok := os.LookupEnv("XUI_PORT")
+	if !ok || strings.TrimSpace(value) == "" {
+		return 0, false, nil
+	}
+
+	port, err = strconv.Atoi(strings.TrimSpace(value))
+	if err != nil {
+		return 0, true, fmt.Errorf("parse XUI_PORT: %w", err)
+	}
+	if port < 1 || port > 65535 {
+		return 0, true, fmt.Errorf("XUI_PORT must be between 1 and 65535")
+	}
+
+	return port, true, nil
+}
+
 // GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
 func GetBinFolderPath() string {
 	binFolderPath := os.Getenv("XUI_BIN_FOLDER")

+ 61 - 0
internal/config/config_test.go

@@ -0,0 +1,61 @@
+package config
+
+import (
+	"os"
+	"testing"
+)
+
+func TestGetPortOverride(t *testing.T) {
+	tests := []struct {
+		name       string
+		value      string
+		set        bool
+		wantPort   int
+		configured bool
+		wantErr    bool
+	}{
+		{name: "unset"},
+		{name: "empty", value: "", set: true},
+		{name: "whitespace", value: "   ", set: true},
+		{name: "minimum", value: "1", set: true, wantPort: 1, configured: true},
+		{name: "default panel port", value: "2053", set: true, wantPort: 2053, configured: true},
+		{name: "surrounding whitespace", value: " 8080 ", set: true, wantPort: 8080, configured: true},
+		{name: "maximum", value: "65535", set: true, wantPort: 65535, configured: true},
+		{name: "zero", value: "0", set: true, configured: true, wantErr: true},
+		{name: "above maximum", value: "65536", set: true, configured: true, wantErr: true},
+		{name: "negative", value: "-1", set: true, configured: true, wantErr: true},
+		{name: "non-numeric", value: "abc", set: true, configured: true, wantErr: true},
+		{name: "decimal", value: "8080.0", set: true, configured: true, wantErr: true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.set {
+				t.Setenv("XUI_PORT", tt.value)
+			} else {
+				original, existed := os.LookupEnv("XUI_PORT")
+				if err := os.Unsetenv("XUI_PORT"); err != nil {
+					t.Fatalf("unset XUI_PORT: %v", err)
+				}
+				t.Cleanup(func() {
+					if existed {
+						_ = os.Setenv("XUI_PORT", original)
+					} else {
+						_ = os.Unsetenv("XUI_PORT")
+					}
+				})
+			}
+
+			port, configured, err := GetPortOverride()
+			if port != tt.wantPort {
+				t.Errorf("port = %d, want %d", port, tt.wantPort)
+			}
+			if configured != tt.configured {
+				t.Errorf("configured = %t, want %t", configured, tt.configured)
+			}
+			if (err != nil) != tt.wantErr {
+				t.Errorf("error = %v, wantErr %t", err, tt.wantErr)
+			}
+		})
+	}
+}

+ 8 - 0
internal/web/web.go

@@ -407,6 +407,14 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) {
 	if err != nil {
 		return err
 	}
+	if envPort, configured, envErr := config.GetPortOverride(); configured {
+		if envErr != nil {
+			logger.Warning("Ignoring invalid XUI_PORT; using configured web port:", port, envErr)
+		} else {
+			port = envPort
+			logger.Info("Using XUI_PORT override for web panel port:", port)
+		}
+	}
 	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
 	listener, err := net.Listen("tcp", listenAddr)
 	if err != nil {