Install
openclaw skills install 3x-ui-vpsDeploy and manage 3X-UI on a root-managed Ubuntu or Debian VPS using Docker Compose, nginx, ACME certificates, SSH panel tunneling, UFW hardening, and Xray VLESS over XHTTP behind nginx. Use when the user explicitly wants to install 3X-UI from scratch, lock the panel and subscription server to 127.0.0.1, open an SSH tunnel to the panel, create or repair a VLESS inbound behind nginx on public 443, add extra clients to an existing inbound, or run safe OS and container updates.
openclaw skills install 3x-ui-vpsDeploy 3X-UI on a VPS with the panel and subscription server bound to loopback, ufw allowing only SSH/HTTP/HTTPS, nginx on public 80/443, and one VLESS + XHTTP transport routed through nginx.
This skill is manual-first because it mutates remote infrastructure. Invoke it only when the user explicitly asks to deploy, repair, harden, or update a VPS.
Collect these before doing any work:
ssh target for the VPS, preferably root@hostAssume Ubuntu or Debian with apt. Do not use this skill on other distributions without adapting the Docker repository setup first.
The operator workstation and the target VPS also need outbound internet access for Docker downloads, ACME issuance, and panel API calls.
Before changing the host, confirm these assumptions:
acme.sh "Domains not changed" as a hard failureUse scripts/bootstrap-host.sh.
Example:
./scripts/bootstrap-host.sh \
--host root@example-vps \
--ssh-password 'host-password' \
--domain vpn.example.com \
--panel-username admin \
--panel-password 'panel-secret'
Default shape:
/opt/3x-uiget.docker.comnetwork_mode: host3x-ui-data/db/3x-ui-data/cert/127.0.0.1:<panel_port>127.0.0.1:2096dig before ACME issuance443401 for unmatched trafficgrpc_passufw must allow only SSH, HTTP, and HTTPS from the internetAfter deploy, verify:
ssh <target> 'ss -ltnp | egrep ":2053 |:2096 |:1234 "'
ssh <target> 'docker compose -f /opt/3x-ui/docker-compose.yml ps'
ssh <target> 'curl -I http://127.0.0.1:2053/'
ssh <target> 'ufw status numbered'
Read references/architecture.md if you need the full topology or nginx routing rationale.
Use scripts/open-panel-tunnel.sh and keep the panel SSH-only.
Default tunnel:
./scripts/open-panel-tunnel.sh --host root@example-vps --ssh-password 'host-password' --local-port 12053 --panel-port 2053
Then open http://127.0.0.1:12053.
If the tunnel fails with an immediate SSH disconnect, avoid parallel SSH sessions to the same host for a short period and retry the tunnel as a single connection after a brief pause.
Do not publish the panel in nginx. If the operator later wants a public panel, treat that as a separate hardening decision.
Use scripts/bootstrap-inbound.py against the tunneled panel URL.
Important detail:
TLS because nginx terminates TLS on 443XHTTP on loopbackPreferred flow:
python3 scripts/bootstrap-inbound.py \
--panel-url http://127.0.0.1:12053 \
--username admin \
--password 'secret' \
--public-domain vpn.example.com \
--backend-port 1234 \
--path /xhttp-keep-this-secret
The script prefers API automation but always prints a manual fallback checklist. Use references/manual-bootstrap.md if API endpoints drift or the panel UI has changed.
That fallback is UI-only. If server-side behavior needs to change, update the bundled scripts first and rerun them.
Use scripts/add-inbound-client.py against the tunneled panel URL.
This workflow is for adding one more client to an already working inbound without changing nginx, ports, or the existing secret path.
Preferred flow:
python3 scripts/add-inbound-client.py \
--panel-url http://127.0.0.1:12053 \
--username admin \
--password 'secret' \
--inbound-id 1
Behavior:
settings.clientsvless:// client URLIf --inbound-id is omitted, the script may auto-select the inbound only when the panel has exactly one inbound. Otherwise require the operator to pass the inbound ID explicitly.
The update workflow must stay conservative:
./scripts/update-stack.sh --host root@example-vps --ssh-password 'host-password'
This runs:
apt updateapt upgradedocker compose pulldocker compose up -d2096ufw rules for SSH, HTTP, and HTTPS onlyDo not switch this skill to apt full-upgrade unless the user explicitly asks for it.
Use these checks before assuming the deploy is broken:
ssh <target> 'ss -ltnp | egrep ":2053 |:2096 |:1234 |:443 |:80 "'ssh <target> 'docker compose -f /opt/3x-ui/docker-compose.yml ps'ssh <target> 'curl -I http://127.0.0.1:2053/'ssh <target> 'cat /opt/3x-ui/bootstrap.env'lsof -nP -iTCP:12053 -sTCP:LISTENInterpretation:
127.0.0.1:2053 and 127.0.0.1:2096 mean panel and sub server are correctly isolated127.0.0.1:1234 means the Xray backend inbound exists0.0.0.0:80 and 0.0.0.0:443 should belong to nginxcurl -I http://127.0.0.1:2053/ returning 404 is acceptable and proves the panel is respondinghttps://<domain>/ returning 401 is the expected nginx default for unmatched traffic--ssh-password instead of wrapping SSH manually.curl -fsSL https://get.docker.com -o get-docker.sh followed by sh ./get-docker.sh.dig, not from the operator workstation, before relying on DNS results.127.0.0.1; verify with ss -ltnp.127.0.0.1:2096; verify with ss -ltnp.80/443.127.0.0.1:1234.nginx default responses normal HTTP 401, not 444, so browsers receive a valid error page.ufw active and restricted to SSH, HTTP, and HTTPS ingress only./app/x-ui setting ...; the x-ui wrapper may not apply panel settings correctly inside the container.subListen=127.0.0.1 and verify that 2096 is not public.scripts/bootstrap-host.sh: install host packages, Docker, nginx, ACME, Compose stack, and nginx configscripts/ssh-with-password.sh: wrapper for ssh with optional plain-text password supportscripts/open-panel-tunnel.sh: open an SSH local port forward to the loopback-bound panelscripts/bootstrap-inbound.py: log in to 3X-UI and create one VLESS client plus inboundscripts/add-inbound-client.py: log in to 3X-UI, load an existing inbound, append one more client, and print the new vless:// URLscripts/update-stack.sh: run safe package and container updates remotelyreferences/architecture.md: deploy topology and nginx behaviorreferences/manual-bootstrap.md: panel UI fallback steps and field mapping