--- name: xui-deploy description: Automates installing and configuring x-ui (Xray panel) on a VPS via SSH. Use when the user wants to deploy x-ui, set up a proxy panel, install 3x-ui, configure Xray on a VPS, or manage inbound proxies. Triggers on phrases like "deploy x-ui", "install x-ui", "setup xui", "install 3x-ui", "xray panel", "部署x-ui", "安装x-ui", "搭建xui", "安装3x-ui", "xray面板", "代理面板". compatibility: Requires SSH access to a Linux VPS (Debian/Ubuntu/CentOS). curl must be available on the VPS. metadata: author: common-skills version: "1.1" --- # x-ui Deploy Automate installing and configuring [x-ui](https://github.com/MHSanaei/3x-ui) (3x-ui fork) on a remote VPS over SSH. For Xray inbound protocol recommendations and configuration details, see [references/xray-inbound-config.md](references/xray-inbound-config.md). Accumulated experience, known issues, and proven configurations are in [references/lessons-learned.md](references/lessons-learned.md). ## Experience Base ### Reading (before starting) Always read `references/lessons-learned.md` before executing any workflow step. Check: - **VPS 环境备注**: if the target host is listed, apply any noted special handling upfront and skip redundant steps - **已知问题**: pre-empt known failure modes for the detected OS and install method - **推荐配置**: offer the user any saved configurations that match their scenario ### Writing (after resolving issues or finding good configs) After any session where a problem was solved or a good configuration was validated, ask the user: > "要把这个经验/配置记录到经验沉淀吗?下次部署可以直接复用。" If yes, append to the appropriate section in `references/lessons-learned.md` following the format in that file. Include: date, environment, symptom, cause, and solution (for issues) or scenario, parameters, and notes (for configs). ## Inputs Collect from the user before starting: | Field | Example | Default | |---|---|---| | VPS host | `123.45.67.89` or `vps.example.com` | — | | SSH user | `root` | `root` | | SSH private key path | `~/.ssh/id_ed25519` | `~/.ssh/id_ed25519` | | SSH port | `22` | `22` | | x-ui panel port | `54321` | `54321` | | x-ui username | `admin` | `admin` | | x-ui password | (from KeePass or user provides) | — | | x-ui web base path | `/xui/` | `/xui/` | **Sensitive credentials (password, username) must be retrieved via `kp-get.sh`**, not typed inline or guessed. Ask the user for the KeePass entry title if not provided. ```bash PANEL_USER=$(bash /repo/common-skills/tools/kp-get.sh "Entry Title" UserName) PANEL_PASS=$(bash /repo/common-skills/tools/kp-get.sh "Entry Title" Password) ``` If the user does not have a KeePass entry for this VPS, ask them to provide the credentials directly and remind them to store them in KeePass afterward. Never hardcode or echo credentials in output. ## Workflow ### 1. Test SSH Connectivity ```bash ssh -i -p -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ @ "echo OK && uname -a" ``` If this fails, stop and report the error. Do not proceed. ### 2. Check OS Compatibility ```bash ssh -i -p @ \ "cat /etc/os-release | grep -E '^(ID|VERSION_ID)'" ``` Supported: Debian 9+, Ubuntu 18.04+, CentOS 7+, AlmaLinux 8+. Warn the user if the OS is unsupported but continue if they confirm. ### 3. Detect Existing x-ui Installation ```bash ssh -i -p @ " if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'x-ui|3x-ui'; then echo 'DOCKER' && docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -E 'x-ui|3x-ui' elif command -v x-ui &>/dev/null; then echo 'NATIVE' && x-ui version else echo 'NOT_INSTALLED' fi " ``` Based on the result, **ask the user what to do**: - **`DOCKER`** — Options: 1) Update config (Step 5) 2) Redeploy from scratch 3) Cancel - **`NATIVE`** — Options: 1) Update config (Step 5) 2) Redeploy from scratch 3) Cancel - **`NOT_INSTALLED`** — Proceed to Step 4. If **Redeploy**, clean up first: ```bash # Docker cleanup ssh -i -p @ " docker stop x-ui 2>/dev/null || true docker rm x-ui 2>/dev/null || true docker rmi ghcr.io/mhsanaei/3x-ui:latest 2>/dev/null || true rm -rf ~/x-ui " # Native cleanup ssh -i -p @ " x-ui stop 2>/dev/null || true systemctl disable x-ui 2>/dev/null || true rm -f /usr/local/bin/x-ui /usr/local/x-ui/x-ui.db rm -f /etc/systemd/system/x-ui.service systemctl daemon-reload " ``` ### 4. Install x-ui **Prefer Docker**. Fall back to native only if Docker is unavailable or fails. #### 4a. Docker install (preferred) > ⚠️ Always use `-p 127.0.0.1::` — never `--network host`. Host networking exposes the panel port publicly and requires iptables workarounds. ```bash # Install Docker if missing ssh -i -p @ \ "docker --version 2>/dev/null || curl -fsSL https://get.docker.com | sh" # Run container with localhost-only port binding ssh -i -p @ " mkdir -p ~/x-ui/db ~/x-ui/cert docker run -d \ --name x-ui \ --restart unless-stopped \ -p 127.0.0.1:: \ -v ~/x-ui/db:/etc/x-ui \ -v ~/x-ui/cert:/root/cert \ ghcr.io/mhsanaei/3x-ui:latest " ``` #### 4b. Native install (fallback) ```bash ssh -i -p @ \ "bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/master/install.sh) <<< $'\n'" ``` If CentOS 7 fails, run `yum install -y epel-release` first and retry. ### 5. Configure Panel Settings > ⚠️ For Docker: use `/app/x-ui` (the binary), not `x-ui` (the shell wrapper which ignores subcommand args). > ⚠️ Username and password must be set in a single command — setting them separately fails. ```bash PANEL_USER=$(bash /repo/common-skills/tools/kp-get.sh "" UserName) PANEL_PASS=$(bash /repo/common-skills/tools/kp-get.sh "" Password) # Docker ssh -i -p @ "docker exec x-ui /app/x-ui setting -port " ssh -i -p @ "docker exec x-ui /app/x-ui setting -username '$PANEL_USER' -password '$PANEL_PASS'" ssh -i -p @ "docker exec x-ui /app/x-ui setting -webBasePath " # Native ssh -i -p @ "x-ui setting -port " ssh -i -p @ "x-ui setting -username '$PANEL_USER' -password '$PANEL_PASS'" ssh -i -p @ "x-ui setting -webBasePath " ``` ### 6. Set Random Subscription Path Do this immediately after panel config — eliminates the "subscription URI insecure" security warning. > ⚠️ Only write `subPath`. Do NOT write `subURI` — it causes the settings page to go blank. ```bash RAND=$(cat /proc/sys/kernel/random/uuid | tr -d '-' | head -c 12) ssh -i -p @ " docker exec x-ui sqlite3 /etc/x-ui/x-ui.db \ \"INSERT OR REPLACE INTO settings (key, value) VALUES ('subPath', '/$RAND/');\" " echo "Subscription path: /$RAND/" ``` ### 7. Restart & Verify ```bash # Docker ssh -i -p @ "docker restart x-ui && sleep 3 && docker ps --filter name=x-ui --format '{{.Status}}'" # Native ssh -i -p @ "x-ui restart && x-ui status" ``` If not running: `docker logs x-ui --tail 30` or `x-ui log`. ### 8. Restrict Panel Port to Localhost For **Docker with `-p 127.0.0.1:port:port`**: already localhost-only, skip this step. For **Docker with `--network host`** or **native install**: ```bash ssh -i -p @ " sudo iptables -I INPUT -p tcp --dport ! -s 127.0.0.1 -j DROP # Persist rules if command -v netfilter-persistent &>/dev/null; then sudo netfilter-persistent save else echo 'WARNING: netfilter-persistent not installed — iptables rule will not survive reboot' echo 'Install with: sudo apt-get install -y iptables-persistent' fi " ``` To access the panel remotely via SSH tunnel: ```bash ssh -i -p -L :127.0.0.1: @ -N ``` ### 9. Set Up Nginx Reverse Proxy Ask the user if they have a domain. If yes, set up HTTPS. If no, set up HTTP-only. #### 9a. Install Nginx if missing ```bash ssh -i -p @ " command -v nginx &>/dev/null || (apt-get install -y nginx 2>/dev/null || yum install -y nginx) sudo systemctl enable --now nginx " ``` #### 9b. Write config (HTTP first, for certbot validation) ```bash ssh -i -p @ "sudo tee /etc/nginx/sites-available/ > /dev/null << 'EOF' server { listen 80; server_name ; location { proxy_pass http://127.0.0.1:; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } EOF sudo ln -sf /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx" ``` #### 9c. HTTPS with Let's Encrypt ```bash ssh -i -p @ " command -v certbot &>/dev/null || apt-get install -y certbot python3-certbot-nginx sudo certbot --nginx -d --non-interactive --agree-tos -m " ``` #### 9d. Final Nginx config (HTTPS + panel + WebSocket) After certbot, update the config to include the WS inbound location: ```nginx server { listen 80; server_name ; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name ; ssl_certificate /etc/letsencrypt/live//fullchain.pem; ssl_certificate_key /etc/letsencrypt/live//privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location { proxy_pass http://127.0.0.1:; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location { proxy_pass http://127.0.0.1:; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 86400s; } } ``` Verify certbot auto-renewal: `sudo certbot renew --dry-run` ### 10. Post-Deploy Security Check ```bash ssh -i -p @ " echo '=== Panel port exposure ===' ss -tlnp | grep ':' | grep -v '127.0.0.1' && echo 'WARNING: panel port publicly exposed!' || echo 'OK: localhost-only' echo '=== Root login ===' grep '^PermitRootLogin' /etc/ssh/sshd_config || echo 'not explicitly set' echo '=== Password auth ===' grep '^PasswordAuthentication' /etc/ssh/sshd_config || echo 'not explicitly set' " ``` Flag as risks: - Panel port on `0.0.0.0`/`*` → **CRITICAL**: apply iptables rule from Step 8 - `PasswordAuthentication yes` → recommend disabling (key-only auth) - `PermitRootLogin yes` → recommend `prohibit-password` ### 11. Configure Xray Inbound Ask the user: > "要设置 Xray 入站协议吗?推荐 VLESS+WebSocket+TLS(有域名/CDN)或 VLESS+Reality(无域名)。" Follow [references/xray-inbound-config.md](references/xray-inbound-config.md) for full steps. **Key notes for this version's API** (confirmed working): - Login endpoint: `POST login` - Add inbound: `POST panel/api/inbounds/add` - List inbounds: `GET panel/api/inbounds/list` - Use `/app/x-ui` binary inside Docker container, not the `x-ui` shell wrapper **WS path must be randomized** — never use `/ws/`. Generate a random path: ```bash WS_PATH="/$(cat /proc/sys/kernel/random/uuid | tr -d '-' | head -c 8)/" ``` After creating the inbound, output the VLESS share link and optionally generate a QR code: ```bash # Generate QR in terminal (requires qrencode) echo "vless://..." | qrencode -t ansiutf8 ``` ### 12. Report Results - Panel URL: `https://` - SSH tunnel (direct): `ssh -L :127.0.0.1: @ -p -N` - VLESS share link + QR code - Certbot renewal status ## Edge Cases - **Docker `--network host` (legacy/existing)**: Panel port will be on `0.0.0.0`. Apply iptables rule in Step 8. For new deployments always use `-p 127.0.0.1:port:port`. - **`x-ui setting` shows help instead of applying**: Use `/app/x-ui setting` directly inside Docker container. - **Username/password setting fails**: Must pass `-username` and `-password` together in one command. - **Settings page blank after DB edit**: Check for invalid keys — only valid keys are `secret`, `webPort`, `webBasePath`, `subPath`. Delete any others and restart. - **API 404 on `/xui/API/inbounds`**: Correct path is `panel/api/inbounds/add`. - **Port already in use**: Step 7 shows bind error. Choose different port, re-run Steps 5–7. - **CentOS 7**: Run `yum install -y epel-release` before native install. - **Non-root SSH user**: Prefix commands with `sudo`. - **Cloud provider security groups (Oracle/AWS/GCP)**: Do not use ufw/firewalld for panel port — manage via cloud console. Use iptables only for localhost restriction. ## Security Notes - Always retrieve credentials via `kp-get.sh`. Never echo passwords. - Docker: use `-p 127.0.0.1:port:port` to enforce localhost binding at container level. - Native: use iptables to block external panel access. Install `iptables-persistent` to survive reboots. - Randomize WS path and subscription path — never use defaults. - Enable HTTPS (Step 9) — direct port access only via SSH tunnel. - `PasswordAuthentication yes` in sshd_config is a risk — recommend key-only. - Panel port ≠ SSH port.