3x-ui + VLESS REALITY 单节点部署教程
一台海外 VPS,一个 VLESS + REALITY 节点,自己的手机、电脑、NAS 共用。不需要域名,主线六步,做完就能上网。
不涉及多节点、多用户分发、流量计费、订阅转换那一套。
开始之前
REALITY 是怎么回事
客户端的 SNI 填一个真实大站,握手验证通过就走代理,验证不通过(比如探测器来敲门)原样转发给那个站。所以自己不用域名,也不用证书。
实测版本
3x-ui v3.7.0(自带 Xray-core 26.7.28)、客户端 v2rayN / v2rayNG / teddysun/xray:26.7.11。安装脚本默认装最新版,新版和本文对不上时以这里的版本号为准回退。
三条硬约束
不照做一定卡住:
| 约束 | 不照做会怎样 |
|---|---|
入站的 mldsa65Seed 必须清空 | 新版建入站时会自动生成,不清空客户端报 reality verification failed |
客户端只用 xray 系:v2rayN、v2rayNG、teddysun/xray | mihomo、NekoBox、sing-box 在这套环境下连不上,别在它们身上耗时间 |
| 时间必须同步 | REALITY 握手带时间校验,时钟偏差大直接连不上 |
需要准备
一台海外 VPS,在商家控制台重装成 Ubuntu 22.04 或 Debian 12,拿到 root 密码或商家注入的 .pem 私钥;本地一个 SSH 客户端(终端 / FinalShell / Xshell)。
还没有 VPS 的话,本文用的是 DMIT 洛杉矶机房(推广链接,走这个下单我会拿到返利,不影响你的价格)。选它是因为 CN2 GIA 线路国内延迟低,其他商家同样适用本文流程。
先定好这几个值
动手前填掉,后面所有命令直接替换:
| 占位符 | 是什么 | 怎么定 |
|---|---|---|
<VPS_IP> | VPS 公网 IPv4 | 商家控制台 |
<PANEL_PORT> | 面板端口 | 自选,10000 以上随机 |
<NODE_PORT> | 节点端口 | 自选。443 最像正常网站,但移动网络下易受干扰;备选 8443 或 20000 以上随机 |
<目标站> | REALITY 伪装的目标站 | 选一个支持 TLS 1.3 的大站,如 www.samsung.com、www.microsoft.com、www.icloud.com。VPS 上 curl -I https://<目标站> 能通即可 |
<客户端名> | 客户端备注 | 随意,英文无空格 |
要不要做订阅,现在就决定——它决定第二步的安装问答怎么答。只有一两台设备、扫二维码就够的,跳过订阅,80 端口也不用开;设备多、想改一次服务端所有设备跟着更新的,做订阅,额外需要一个 <SUB_PORT>(自选),且 80 端口要常开给证书续期。
还有一组值不用自己想,第五步在面板里点按钮生成:<UUID>、<SUB_ID>、<REALITY_PUBLIC_KEY>、<REALITY_PRIVATE_KEY>、<SHORT_ID_1> 到 <SHORT_ID_3>。
以下命令默认 root 执行。
一、SSH 连接与加固
ssh root@<VPS_IP> 连上(密钥登录导入 .pem,密码登录直接输)。
先跑商家要求的初始化。DMIT 的机器重装后要装 guest agent,否则控制台的重启、密码重置、快照都不生效:
apt update && apt install -y qemu-guest-agent cloud-initsystemctl enable qemu-guest-agentsystemctl start qemu-guest-agent其他商家按各自控制台的说明处理,没有要求就跳过。
确认密钥能登录后,关掉密码登录:
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_configsed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_configsed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_configsed -i 's/^#\?ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_configsed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_configsshd -t && systemctl restart ssh别关当前窗口,另开一个连接确认能登上再继续,否则改错了会把自己锁在外面。
装好基础工具并开启时间同步:
apt install -y curl wget socattimedatectl set-ntp true时间同步别跳,这是上面第三条硬约束。
二、安装 3x-ui
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)安装问答:
| 提示 | 回答 |
|---|---|
| Database Selection | 1(SQLite) |
| 自定义面板端口 | y,端口 <PANEL_PORT>,路径自定(建议随机字符串,别用 admin) |
| SSL 方式 | 做订阅选 2(Let’s Encrypt for IP);不做订阅回车跳过 |
| IP 是否正确 | y |
| IPv6 | 回车跳过 |
| ACME 端口 | 回车(默认 80,不要改,Let’s Encrypt 只连 80) |
装完的账号密码在 /etc/x-ui/install-result.env。
把面板改成只监听本机,之后只能通过 SSH 隧道访问:
x-ui setting -listen 127.0.0.1x-ui restartx-ui setting -show三、防火墙
x-ui选「防火墙管理」,安装防火墙,开放端口依次输入 <NODE_PORT>(做订阅的再加 80 和 <SUB_PORT>),启用防火墙。退出菜单后收紧规则:
ufw limit 22/tcpufw delete allow <NODE_PORT> && ufw allow <NODE_PORT>/tcpufw delete allow <PANEL_PORT> 2>/dev/null; ufw limit <PANEL_PORT>/tcpufw status最终规则:
| 端口 | 动作 | 用途 |
|---|---|---|
| 22/tcp | LIMIT | SSH,同一 IP 30 秒内连 6 次以上自动拒绝 |
<NODE_PORT>/tcp | ALLOW | REALITY 节点 |
<PANEL_PORT>/tcp | LIMIT | 面板(实际只监听本机,规则仅备用) |
| 80/tcp | ALLOW | 仅做订阅时需要,Let’s Encrypt 每 6 天续期 |
<SUB_PORT>/tcp | ALLOW | 仅做订阅时需要 |
80 常开没问题:acme.sh 只在续期那几秒监听,平时这个口上没有服务。
四、进面板并加固
建一条 SSH 隧道:
ssh -N -L <PANEL_PORT>:127.0.0.1:<PANEL_PORT> root@<VPS_IP>FinalShell / Xshell 在连接的「隧道」页填同样的本地端口、远程 127.0.0.1、远程端口。然后浏览器打开 http://127.0.0.1:<PANEL_PORT>/<面板路径>。
进去后四件事,顺序别换:
x-ui菜单 7 改用户名密码(先做这步,改密码会关掉 2FA)- 面板 → 设置 → 安全 → 开 2FA
- 会话时长改 60 分钟
x-ui菜单 26 开 BBR
五、建入站
入站 → 添加入站 → 协议 vless、安全 Reality,点「获取新证书」和 Short IDs 的刷新按钮生成密钥和 shortId,UUID、订阅 ID 自动就有。把这些值填进下面的占位符,用「导入」粘贴,或者照着在表单里逐项填。
{ "listen": "", "port": <NODE_PORT>, "protocol": "vless", "tag": "in-<NODE_PORT>-tcp", "settings": { "clients": [ { "id": "<UUID>", "email": "<客户端名>", "flow": "xtls-rprx-vision", "limitIp": 2, "totalGB": 0, "expiryTime": 0, "enable": true, "tgId": 0, "subId": "<SUB_ID>", "comment": "", "reset": 0 } ], "decryption": "none", "encryption": "none" }, "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"], "metadataOnly": false, "routeOnly": false }, "streamSettings": { "network": "tcp", "tcpSettings": { "acceptProxyProtocol": false, "header": { "type": "none" } }, "security": "reality", "realitySettings": { "show": false, "xver": 0, "target": "<目标站>:443", "serverNames": ["<目标站>"], "privateKey": "<REALITY_PRIVATE_KEY>", "minClientVer": "", "maxClientVer": "", "maxTimediff": 0, "shortIds": ["<SHORT_ID_1>", "<SHORT_ID_2>", "<SHORT_ID_3>"], "mldsa65Seed": "", "settings": { "publicKey": "<REALITY_PUBLIC_KEY>", "fingerprint": "chrome", "serverName": "<目标站>", "spiderX": "/" } }, "externalProxy": [] }}导入后填个备注,保存,然后 x-ui restart。
四个填表时最容易出错的地方:
mldsa65Seed留空,这是第一条硬约束。minClientVer/maxClientVer也留空。表单里灰色的26.3.27是占位符不是值;新版面板这两项的默认值互相冲突,会直接掐掉桌面客户端的连接(issue #6069)。- 面板的「扫描」或「查找目标」会给出一串 SNI,删到只剩
<目标站>一个,和target保持一致。 limitIp按自己的设备数填,手机加电脑加 NAS 一般是 3。超出的源 IP 由 fail2ban 封 30 分钟。
六、客户端连上
| 系统 | 客户端 |
|---|---|
| Windows | v2rayN(2dust/v2rayN 的 v2rayN-windows-64.zip) |
| Android | v2rayNG(2dust/v2rayNG 的 arm64-v8a.apk) |
| macOS | V2rayU 或 Streisand |
不要用 NekoBox,本文环境下连不上。
入站列表 → 客户端 → 扫二维码导入。连上后打开 https://ip.sb,显示的是 <VPS_IP> 就成了。
到这里节点已经能用了。 下面两节是可选的。
可选:订阅链接
设备多的时候值得做——改一次服务端,所有设备跟着更新,不用挨个重扫码。
面板 → 设置 → 订阅设置:
| 项 | 值 |
|---|---|
| 启用订阅 | 开 |
| 监听 IP | 留空 |
| 端口 | <SUB_PORT> |
| 域名 | <VPS_IP> |
| 路径 | /sub/ |
| 证书公钥 | /root/cert/<VPS_IP>/fullchain.pem |
| 证书私钥 | /root/cert/<VPS_IP>/privkey.pem |
| 备注模板 | {{REMARK}} |
证书路径不确定就 ls /root/cert 看一眼。订阅链接是 https://<VPS_IP>:<SUB_PORT>/sub/<SUB_ID>。
REALITY 节点本身不用证书,这里的证书只给订阅链接用。
可选:NAS 上的容器走代理
在 NAS 上跑一个 xray 容器,启动时拉订阅自动生成配置,对局域网提供 HTTP / SOCKS 代理 <NAS_IP>:<PROXY_PORT>,其他容器用环境变量指向它。需要先做上面的订阅。
两个额外的值:<NAS_IP>(NAS 局域网 IP)、<PROXY_PORT>(代理端口,避开 10808 / 10809 / 7890 / 1080,选 20000 以上随机)。
起这个容器
镜像钉死版本号而不是 latest,免得哪天镜像更新引入新问题;要升级时改这里的 tag。
脚本内联在 compose 里,不挂载外部文件(Dokploy 的 File Mount 会被 Docker 建成同名目录)。$ 已经转义成 $$,原样粘贴:
services: xray: image: teddysun/xray:26.7.11 container_name: xray restart: unless-stopped environment: - TZ=Asia/Shanghai - SUB_URL=https://<VPS_IP>:<SUB_PORT>/sub/<SUB_ID> - PROXY_PORT=<PROXY_PORT> ports: - "<PROXY_PORT>:<PROXY_PORT>" entrypoint: ["/bin/sh", "-c"] command: - | set -e echo "fetching subscription: $$SUB_URL" SUB=$$(wget --no-check-certificate -O - "$$SUB_URL") || { echo "wget failed"; exit 1; } echo "raw length: $${#SUB}" DECODED=$$(echo "$$SUB" | base64 -d 2>/dev/null || echo "$$SUB") VLESS_URL=$$(echo "$$DECODED" | grep "^vless://" | head -1) [ -z "$$VLESS_URL" ] && { echo "no vless node"; echo "$$DECODED" | head -5; exit 1; } echo "node: $${VLESS_URL%%\?*}" URL="$${VLESS_URL#vless://}" URL="$${URL%%#*}" UUID="$${URL%%@*}" URL="$${URL#*@}" HOST="$${URL%%:*}" URL="$${URL#*:}" PORT="$${URL%%\?*}" PARAMS="$${URL#*\?}" gp() { echo "$$PARAMS" | tr '&' '\n' | grep "^$$1=" | cut -d'=' -f2; } FLOW=$$(gp flow); SECURITY=$$(gp security); SNI=$$(gp sni) PBK=$$(gp pbk); SID=$$(gp sid); FP=$$(gp fp); TYPE=$$(gp type) echo "host=$$HOST port=$$PORT sni=$$SNI flow=$$FLOW sid=$$SID fp=$${FP:-chrome}" cat > /etc/xray/config.json << EOF { "log": { "loglevel": "warning" }, "inbounds": [{ "tag": "mixed-in", "listen": "0.0.0.0", "port": $$PROXY_PORT, "protocol": "mixed", "settings": { "udp": true }, "sniffing": { "enabled": true, "destOverride": ["http", "tls"] } }], "outbounds": [ { "tag": "proxy", "protocol": "vless", "settings": { "vnext": [{ "address": "$$HOST", "port": $$PORT, "users": [{"id": "$$UUID", "flow": "$$FLOW", "encryption": "none"}] }] }, "streamSettings": { "network": "$${TYPE:-tcp}", "security": "$$SECURITY", "realitySettings": { "serverName": "$$SNI", "fingerprint": "$${FP:-chrome}", "publicKey": "$$PBK", "shortId": "$$SID", "spiderX": "/" } } }, { "tag": "direct", "protocol": "freedom" } ], "routing": { "domainStrategy": "IPIfNonMatch", "rules": [ { "type": "field", "ip": ["$$HOST/32", "geoip:private"], "outboundTag": "direct" }, { "type": "field", "domain": ["geosite:cn"], "outboundTag": "direct" }, { "type": "field", "ip": ["geoip:cn"], "outboundTag": "direct" } ] } } EOF echo "config written, starting xray" exec xray run -c /etc/xray/config.json logging: driver: json-file options: max-size: 10m max-file: "3"分流规则:VPS 自身 IP 和内网直连,国内域名和 IP 直连,其余走代理。不分流的话国内流量全绕海外,慢、耗 VPS 流量、支付类服务还容易触发风控。
验证
docker logs xraycurl -s -x http://127.0.0.1:<PROXY_PORT> https://ip.sb # 应返回 <VPS_IP>curl -s -x http://127.0.0.1:<PROXY_PORT> https://myip.ipip.net # 应返回家宽 IP日志依次出现 fetching、node:、config written 就是正常的。服务端改过入站后 docker restart xray 同步。
让别的容器走它
在它的 compose 里加:
- HTTP_PROXY=http://<NAS_IP>:<PROXY_PORT> - HTTPS_PROXY=http://<NAS_IP>:<PROXY_PORT> - NO_PROXY=localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12Dokploy 自己克隆 GitHub 仓库也需要这组变量(加给 dokploy 服务),或者把仓库地址改用 https://ghfast.top/https://github.com/... 镜像。
NO_PROXY 少写一段网段会让容器互访绕到境外再回来——这类容器侧的坑另外记在在 NAS 上用 Coolify 部署的十条约定里。
维护
mkdir -p /root/bakcp /etc/x-ui/x-ui.db /root/bak/x-ui-$(date +%F).db # 升级前备份crontab -l | grep acme # 确认续期任务还在x-ui banlog # 看封禁记录升级面板后检查两件事:mldsa65Seed 有没有被重新填上,客户端还能不能连。
故障速查
| 现象 | 原因 | 处理 |
|---|---|---|
reality verification failed / REALITY authentication failed | mldsa65Seed 有值;或扫的是旧码;或客户端是 mihomo / NekoBox / sing-box 系 | 清空 seed;删节点重扫;换 xray 系客户端 |
| 电脑能用手机不能 | 移动网络对节点端口干扰(443 尤其明显) | 手机连 WiFi 测一次;确认是干扰就把入站换成 8443 或高位随机端口,防火墙同步放行 |
国内外都连不上 <NODE_PORT> | IP 或端口被墙 | ping.pe 分运营商测;换端口或换 IP |
| 面板打不开 | 隧道没建,或 listen 改错 | x-ui setting -show 看 listen 值 |
| 手机订阅拉不下来 | 订阅证书无效或 <SUB_PORT> 未放行 | 检查 /root/cert、ufw status |
| 证书续期失败 | 80 被关或被占 | ufw status、ss -tlnp | grep :80 |
Dokploy 报 gnutls_handshake() failed | NAS 直连 GitHub 被掐 | 给 dokploy 服务加 HTTP_PROXY,或仓库地址走 ghfast.top |
遇到本文没覆盖的报错,把报错原文和 x-ui setting -show、ufw status、docker logs xray 的输出一起交给 AI。
几个选择的理由
为什么是 REALITY
不用域名和证书,部署最省事,抗封效果也好。其他几种各有代价:
| 方案 | 代价 |
|---|---|
| VLESS / Trojan + TLS | 要自备域名和证书 |
| Hysteria2 | 走 UDP,弱网表现最好,但容易被运营商限速 |
| VLESS + WS + TLS + CDN | 抗封最强,但多一跳、慢,且同样要域名 |
为什么 mldsa65Seed 要清空
它是 Xray 给 REALITY 加的抗量子 ML-DSA-65 签名,服务端持有种子,客户端可选验证。
官方说明这一步向后兼容——客户端没配 mldsa65Verify 只是跳过额外验证,仍能连接。但实测 NekoBox / mihomo 在服务端开了 seed 时握手就失败,xray 系客户端正常。
它也不负责流量加密,清空是最省事的选择。(Xray-core PR #4915、xray mldsa65 命令文档)
为什么客户端只能用 xray 系
| 组合 | 结果 |
|---|---|
| 3x-ui 3.5.0(内核变成 Xray 26.7.11)+ REALITY + vision 入站 | 开始拒绝一部分正常客户端,把内核降回 26.6.27 立刻恢复 |
| OpenWrt 上的 sing-box 1.12.17 连 Xray 26.7.11 | 也报同样的错 |
| 路由器上装 PassWall 并选 xray 内核 | 能用 |
| OpenClash(mihomo 内核) | 连不上 |
本文没有验证降级,直接换成 xray 系客户端解决。(3x-ui issue #5943、podkop issue #415)
为什么面板只监听本机
3x-ui 官方安全指南的建议,面板不直接暴露公网,用 SSH 隧道或反向代理访问,开 2FA,路径用随机长串。(3x-ui Security 文档)
为什么订阅要开 80,能不能关
用 IP 证书不能。Let’s Encrypt 的 IP 证书只支持 http-01 和 tls-alpn-01 验证,不支持 DNS 验证;http-01 只会连 80,本地监听改成别的端口也没用。
而且 IP 证书必须用 shortlived 配置,有效期 160 小时,6 天续一次。
要关 80 有两条路:
| 路子 | 代价 |
|---|---|
| tls-alpn-01 走 443 | 续期时短暂让出 443 |
| 买个域名走 DNS-01 | 一个端口都不用开、90 天一续 |
新版 3x-ui 还有哪些改动
| 版本 | 改动 |
|---|---|
| 3.5 起 | REALITY 入站自动生成 mldsa65Seed |
| 3.6 起 | 按 User-Agent 自动判断订阅格式 |
| 3.7 起 | 用浏览器打开订阅链接会返回一个只能复制的页面,要看原始内容加 ?html=1 |
客户端拿到 HTML 解析失败时,订阅地址后加 ?format=base64 强制格式。(3x-ui Releases)
给 AI 的执行说明
如果你是 AI,用户把本文链接给了你:目标是在一台海外 VPS 上搭一个自用的 VLESS + REALITY 节点,单节点单用户,自己几台设备共用。
按「一、SSH 连接与加固」到「六、客户端连上」的顺序做,一次只给一步,用户贴回输出、验证通过再给下一步。
开始前先让用户填好「先定好这几个值」那张表,并问清楚要不要做订阅——这决定第二步安装问答里 SSL 那项怎么答。两节「可选」只在用户明确提出时做。命令默认 root 执行。
三条硬约束(mldsa65Seed 清空、只用 xray 系客户端、时间同步)在每一步相关时主动提醒,它们是这套流程最常见的失败点。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


