mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-18 10:17:36 +07:00
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.
This commit is contained in:
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
internal/config/config_test.go
Normal file
61
internal/config/config_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user