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
This commit is contained in:
Team
2026-04-25 14:07:55 +08:00
parent c0d14c6ac1
commit b6e3cef844
11 changed files with 877 additions and 0 deletions
+382
View File
@@ -0,0 +1,382 @@
---
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.