Files
common-skills/skills/xui-deploy/SKILL.md
T
Team b6e3cef844 feat: add xui-deploy skill with lessons learned
- SKILL.md v1.1: full deployment workflow for 3x-ui on VPS via SSH
- Covers Docker/native install, Nginx+TLS, Xray inbound config
- references/xray-inbound-config.md: VLESS+WS+TLS and Reality configs
- references/lessons-learned.md: lessons from first real deployment
  - /app/x-ui binary vs shell wrapper in Docker
  - correct API path: panel/api/inbounds/add
  - subPath-only DB write (subURI causes blank settings page)
  - --network host port exposure workaround
- Agent prompt and eval configs included
2026-04-25 14:07:55 +08:00

383 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <key_path> -p <ssh_port> -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
<user>@<host> "echo OK && uname -a"
```
If this fails, stop and report the error. Do not proceed.
### 2. Check OS Compatibility
```bash
ssh -i <key_path> -p <ssh_port> <user>@<host> \
"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 <key_path> -p <ssh_port> <user>@<host> "
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 <key_path> -p <ssh_port> <user>@<host> "
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 <key_path> -p <ssh_port> <user>@<host> "
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:<port>:<port>` — never `--network host`. Host networking exposes the panel port publicly and requires iptables workarounds.
```bash
# Install Docker if missing
ssh -i <key_path> -p <ssh_port> <user>@<host> \
"docker --version 2>/dev/null || curl -fsSL https://get.docker.com | sh"
# Run container with localhost-only port binding
ssh -i <key_path> -p <ssh_port> <user>@<host> "
mkdir -p ~/x-ui/db ~/x-ui/cert
docker run -d \
--name x-ui \
--restart unless-stopped \
-p 127.0.0.1:<panel_port>:<panel_port> \
-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 <key_path> -p <ssh_port> <user>@<host> \
"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 "<entry_title>" UserName)
PANEL_PASS=$(bash /repo/common-skills/tools/kp-get.sh "<entry_title>" Password)
# Docker
ssh -i <key_path> -p <ssh_port> <user>@<host> "docker exec x-ui /app/x-ui setting -port <panel_port>"
ssh -i <key_path> -p <ssh_port> <user>@<host> "docker exec x-ui /app/x-ui setting -username '$PANEL_USER' -password '$PANEL_PASS'"
ssh -i <key_path> -p <ssh_port> <user>@<host> "docker exec x-ui /app/x-ui setting -webBasePath <base_path>"
# Native
ssh -i <key_path> -p <ssh_port> <user>@<host> "x-ui setting -port <panel_port>"
ssh -i <key_path> -p <ssh_port> <user>@<host> "x-ui setting -username '$PANEL_USER' -password '$PANEL_PASS'"
ssh -i <key_path> -p <ssh_port> <user>@<host> "x-ui setting -webBasePath <base_path>"
```
### 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 <key_path> -p <ssh_port> <user>@<host> "
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 <key_path> -p <ssh_port> <user>@<host> "docker restart x-ui && sleep 3 && docker ps --filter name=x-ui --format '{{.Status}}'"
# Native
ssh -i <key_path> -p <ssh_port> <user>@<host> "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 <key_path> -p <ssh_port> <user>@<host> "
sudo iptables -I INPUT -p tcp --dport <panel_port> ! -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 <key_path> -p <ssh_port> -L <panel_port>:127.0.0.1:<panel_port> <user>@<host> -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 <key_path> -p <ssh_port> <user>@<host> "
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 <key_path> -p <ssh_port> <user>@<host> "sudo tee /etc/nginx/sites-available/<domain> > /dev/null << 'EOF'
server {
listen 80;
server_name <domain>;
location <base_path> {
proxy_pass http://127.0.0.1:<panel_port><base_path>;
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/<domain> /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx"
```
#### 9c. HTTPS with Let's Encrypt
```bash
ssh -i <key_path> -p <ssh_port> <user>@<host> "
command -v certbot &>/dev/null || apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <domain> --non-interactive --agree-tos -m <admin_email>
"
```
#### 9d. Final Nginx config (HTTPS + panel + WebSocket)
After certbot, update the config to include the WS inbound location:
```nginx
server {
listen 80;
server_name <domain>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name <domain>;
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location <base_path> {
proxy_pass http://127.0.0.1:<panel_port><base_path>;
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 <ws_path> {
proxy_pass http://127.0.0.1:<inbound_port>;
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 <key_path> -p <ssh_port> <user>@<host> "
echo '=== Panel port exposure ==='
ss -tlnp | grep ':<panel_port>' | 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 <base_path>login`
- Add inbound: `POST <base_path>panel/api/inbounds/add`
- List inbounds: `GET <base_path>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://<domain><base_path>`
- SSH tunnel (direct): `ssh -L <panel_port>:127.0.0.1:<panel_port> <user>@<host> -p <ssh_port> -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 `<base_path>panel/api/inbounds/add`.
- **Port already in use**: Step 7 shows bind error. Choose different port, re-run Steps 57.
- **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.