- 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
14 KiB
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. |
|
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) CancelNATIVE— Options: 1) Update config (Step 5) 2) Redeploy from scratch 3) CancelNOT_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), notx-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 writesubURI— 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→ recommendprohibit-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-uibinary inside Docker container, not thex-uishell 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 on0.0.0.0. Apply iptables rule in Step 8. For new deployments always use-p 127.0.0.1:port:port. x-ui settingshows help instead of applying: Use/app/x-ui settingdirectly inside Docker container.- Username/password setting fails: Must pass
-usernameand-passwordtogether 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-releasebefore 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:portto enforce localhost binding at container level. - Native: use iptables to block external panel access. Install
iptables-persistentto survive reboots. - Randomize WS path and subscription path — never use defaults.
- Enable HTTPS (Step 9) — direct port access only via SSH tunnel.
PasswordAuthentication yesin sshd_config is a risk — recommend key-only.- Panel port ≠ SSH port.