Files
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

14 KiB
Raw Permalink Blame History

name, description, compatibility, metadata
name description compatibility metadata
xui-deploy 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面板", "代理面板". Requires SSH access to a Linux VPS (Debian/Ubuntu/CentOS). curl must be available on the VPS.
author version
common-skills 1.1

x-ui Deploy

Automate installing and configuring x-ui (3x-ui fork) on a remote VPS over SSH.

For Xray inbound protocol recommendations and configuration details, see references/xray-inbound-config.md.

Accumulated experience, known issues, and proven configurations are in 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.

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

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

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

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:

# 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.

# 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)

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.

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.

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

# 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:

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:

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

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)

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

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:

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

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 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:

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:

# 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.