3x-ui + VLESS REALITY 单节点部署教程

3563 字
18 分钟
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/xraymihomo、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,否则控制台的重启、密码重置、快照都不生效:

Terminal window
apt update && apt install -y qemu-guest-agent cloud-init
systemctl enable qemu-guest-agent
systemctl start qemu-guest-agent

其他商家按各自控制台的说明处理,没有要求就跳过。

确认密钥能登录后,关掉密码登录:

Terminal window
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
sed -i 's/^#\?ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
sshd -t && systemctl restart ssh
Warning

别关当前窗口,另开一个连接确认能登上再继续,否则改错了会把自己锁在外面。

装好基础工具并开启时间同步:

Terminal window
apt install -y curl wget socat
timedatectl set-ntp true
Important

时间同步别跳,这是上面第三条硬约束。

二、安装 3x-ui#

Terminal window
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)

安装问答:

提示回答
Database Selection1(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 隧道访问:

Terminal window
x-ui setting -listen 127.0.0.1
x-ui restart
x-ui setting -show

三、防火墙#

Terminal window
x-ui

选「防火墙管理」,安装防火墙,开放端口依次输入 <NODE_PORT>(做订阅的再加 80 和 <SUB_PORT>),启用防火墙。退出菜单后收紧规则:

Terminal window
ufw limit 22/tcp
ufw delete allow <NODE_PORT> && ufw allow <NODE_PORT>/tcp
ufw delete allow <PANEL_PORT> 2>/dev/null; ufw limit <PANEL_PORT>/tcp
ufw status

最终规则:

端口动作用途
22/tcpLIMITSSH,同一 IP 30 秒内连 6 次以上自动拒绝
<NODE_PORT>/tcpALLOWREALITY 节点
<PANEL_PORT>/tcpLIMIT面板(实际只监听本机,规则仅备用)
80/tcpALLOW仅做订阅时需要,Let’s Encrypt 每 6 天续期
<SUB_PORT>/tcpALLOW仅做订阅时需要

80 常开没问题:acme.sh 只在续期那几秒监听,平时这个口上没有服务。

四、进面板并加固#

建一条 SSH 隧道:

Terminal window
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>/<面板路径>。

进去后四件事,顺序别换:

  1. x-ui 菜单 7 改用户名密码(先做这步,改密码会关掉 2FA)
  2. 面板 → 设置 → 安全 → 开 2FA
  3. 会话时长改 60 分钟
  4. 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 分钟。

六、客户端连上#

系统客户端
Windowsv2rayN(2dust/v2rayN 的 v2rayN-windows-64.zip)
Androidv2rayNG(2dust/v2rayNG 的 arm64-v8a.apk)
macOSV2rayU 或 Streisand
Warning

不要用 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 流量、支付类服务还容易触发风控。

验证#

Terminal window
docker logs xray
curl -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/12

Dokploy 自己克隆 GitHub 仓库也需要这组变量(加给 dokploy 服务),或者把仓库地址改用 https://ghfast.top/https://github.com/... 镜像。

NO_PROXY 少写一段网段会让容器互访绕到境外再回来——这类容器侧的坑另外记在在 NAS 上用 Coolify 部署的十条约定里。

维护#

Terminal window
mkdir -p /root/bak
cp /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 failedmldsa65Seed 有值;或扫的是旧码;或客户端是 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() failedNAS 直连 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 天一续

(Let’s Encrypt:首张 IP 证书、6 天证书与 IP 证书正式可用)

新版 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 系客户端、时间同步)在每一步相关时主动提醒,它们是这套流程最常见的失败点。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

3x-ui + VLESS REALITY 单节点部署教程
https://blog.always1ov.com/posts/3x-ui-vless-reality-self-hosted-node/
作者
薯饼
发布于
2026-09-01
许可协议
CC BY-NC-SA 4.0