b6e3cef844
- 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
383 lines
14 KiB
Markdown
383 lines
14 KiB
Markdown
---
|
||
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 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.
|