فهرست منبع

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 ساعت پیش
والد
کامیت
7f34c306d7
6فایلهای تغییر یافته به همراه99 افزوده شده و 1 حذف شده
  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 {