过去一段时间,我在 Azure、DigitalOcean 和 GCP 上陆续搭过几台个人 VPS。真正长期跑下来以后,我发现“把服务启动”并不是最难的部分,难的是让它可验证、可升级、出问题时能迅速定位,而且不会因为一次改配置把自己锁在服务器外面。
这篇把我实际跑过的结构整理成一套从零可复现的方案:
- VLESS + REALITY 使用
TCP 443,作为稳定的 TCP 主线路。 - Hysteria 2 使用
UDP 443,适合质量一般或丢包较高的链路。 - TUIC v5 使用
UDP 8443,作为另一条 QUIC 备用线路。 - 三个服务交给 Docker Compose 管理。
- Hysteria 2 和 TUIC 共用一张有效 TLS 证书。
- 云安全组与 VPS 本机防火墙同时收口。
- 客户端使用 Mihomo,Clash Verge Rev、FlClash 等图形客户端可以直接加载。
我会把完整配置放出来,但不会放任何真实节点信息。文中的 203.0.113.10 属于文档保留地址,node.example.com 和所有 CHANGE_ME_* 都必须替换。
适用范围与合规提醒: 这篇只适用于你本人拥有或获得授权的服务器和网络。部署前请确认所在地法律、云厂商服务条款和流量政策。不要把节点用于端口扫描、群发、爬虫滥用、BT 或其他会伤害第三方的行为。
阅读路线
这是一篇从准备到运维的长文,可以按五段阅读:
- “一”到“六”:VPS、变量、SSH、Docker、证书和目录准备。
- “七”到“九”:VLESS Reality、Hysteria 2、TUIC 三个服务端配置。
- “十”到“十三”:Compose、占位符检查、防火墙和启动。
- “十四”到“十五”:Mihomo 客户端和端到端验收。
- “十六”以后:续期、备份、升级、回滚、排错和多节点。
第一次部署做到 VLESS + Hysteria 2 并完成端到端验收即可;TUIC、多节点和链式代理都可以以后再加。
先看最终架构
Mihomo / Clash Verge Rev / FlClash / Shadowrocket
|
云厂商安全组 / 防火墙
|
VPS 公网 IP
|
+-----------+-----------+
| |
TCP 443 UDP 443
VLESS + REALITY Hysteria 2
Xray Hysteria
|
+------------------- UDP 8443
TUIC v5
TCP 和 UDP 是两套独立的端口空间,所以 Xray 可以占用 443/tcp,Hysteria 2 同时占用 443/udp。但 Hysteria 2 和 TUIC 都基于 UDP,不能绑定同一个 IP:UDP 端口,因此 TUIC 放到 8443/udp。
这套结构的价值不在于“协议越多越好”,而在于故障隔离:
| 线路 | 传输 | 端口 | 作用 |
|---|---|---|---|
| VLESS + REALITY | RAW/TCP | 443/tcp | 默认主线路,兼容性好 |
| Hysteria 2 | QUIC/UDP | 443/udp | 丢包环境下的备用或主线路 |
| TUIC v5 | QUIC/UDP | 8443/udp | 第二条 UDP 备用线路 |
如果是第一次部署,可以先完成 VLESS 和 Hysteria 2,验收通过以后再加 TUIC。这样排错会简单很多。
本文核验环境与版本
版本变化很快。下面是我在 2026-07-12 写这篇文章时核对的稳定版本,不代表以后永远应该锁死在这些版本:
| 组件 | 核验时的稳定版本 | 说明 |
|---|---|---|
| Xray-core | v26.3.27 | v26.7.11 当时仍是 Pre-release |
| Hysteria 2 | v2.9.3 | 已包含 v2.9.2 的重要安全修复 |
| TUIC | 协议 v5;Itsusinn 实现 v1.8.10 | TUIC 协议仓库明确没有“官方实现” |
| Mihomo | v1.19.28 | 客户端配置按这一代字段核验 |
| Clash Verge Rev | v2.5.1 | 内置 Mihomo 版本仍需在客户端里确认 |
生产环境不要长期依赖 latest。首次拉取镜像并验证成功后,应记录镜像 digest,再固定到明确版本或 digest。升级前先备份,升级后重新做端到端验收。
还有一个容易踩坑的版本差异:Xray 的滚动文档可能领先于 GitHub 的稳定发行。本文针对 v26.3.27 稳定版,VLESS 入站写的是 settings.clients,不要直接把滚动文档里的新字段无脑替换进来。
一、准备 VPS、域名和本地工具
VPS 最低规格
个人使用可以从下面的规格开始:
- Ubuntu 24.04 LTS 或 Debian 12。
- 1 vCPU。
- 1 GB 内存。
- 10 GB 以上系统盘。
- 一个独立公网 IPv4。
- 每月流量按自己的实际使用量选择。
1 GB 内存能跑这三个轻量服务,但建议加 1 GB swap。若同机还跑面板、机器人或其他服务,最好直接用 2 GB 内存。
选地区时不要只看价格。先从自己常用网络对目标机房做延迟和丢包测试。美国东海岸到东亚出现 200–300 ms RTT,很多时候是物理距离,不是协议坏了。需要长期稳定时,也不建议把 Spot/可抢占实例当唯一节点。
域名
VLESS + REALITY 本身不要求你拥有域名或证书,但 Hysteria 2 和 TUIC 需要 TLS。正式环境建议准备一个子域名,例如:
node.example.com -> 203.0.113.10
如果使用 Cloudflare DNS,这条记录应设为 DNS only。普通 CDN 不能代转 Hysteria 2 或 TUIC 的认证后流量。
本地要准备的东西
- SSH 客户端和一对 SSH 密钥。
- 一个密码管理器,用来保存 UUID、密码、Reality 密钥和 short ID。
- Mihomo 内核,或带 Mihomo 的 Clash Verge Rev / FlClash。
- 一个不会被系统代理绕进去的紧急登录方式,例如云厂商串口控制台。
二、先把所有变量列清楚
正式操作前,我会先建一张变量表。文章里的值全部是占位符:
| 名称 | 示例或用途 |
|---|---|
SERVER_IP | 203.0.113.10,必须替换 |
DOMAIN | node.example.com,必须替换 |
SSH_USER | 非 root 管理用户 |
ALLOWED_ADMIN_CIDR | 你的管理出口,例如 198.51.100.25/32 |
VLESS_UUID | VLESS 用户 UUID |
REALITY_TARGET | 符合要求的 TLS 站点 |
REALITY_PRIVATE_KEY | 只保存在服务端 |
REALITY_PUBLIC_KEY | 只给客户端 |
REALITY_SHORT_ID | 0–16 位、偶数长度的十六进制字符串 |
HY2_PASSWORD | Hysteria 2 强随机密码 |
TUIC_UUID | TUIC v5 用户 UUID |
TUIC_PASSWORD | TUIC v5 强随机密码 |
XRAY_IMAGE_REFERENCE | 已核验 Xray 镜像的完整 digest 引用 |
HY2_IMAGE_REFERENCE | 已核验 Hysteria 2 镜像的完整 digest 引用 |
TUIC_IMAGE_REFERENCE | 已核验 TUIC 镜像的完整 digest 引用 |
在 VPS 上先收紧新文件的默认权限:
umask 077
生成基础凭据:
cat /proc/sys/kernel/random/uuid
cat /proc/sys/kernel/random/uuid
openssl rand -hex 32
openssl rand -hex 32
openssl rand -hex 8
依次把结果保存成 VLESS UUID、TUIC UUID、Hysteria 2 密码、TUIC 密码和 Reality short ID。不要把生成结果直接打印进 cloud-init、串口日志、Git 提交或博客草稿。
Reality 密钥稍后用 Xray 自己生成。不要用固定示例密钥,也不要用脚本按输出列名强行 awk:Xray 的输出字段在不同版本间变过,自动解析很容易把错误值写进配置。
三、SSH 与系统基础加固
更新系统
新机器刚开机时,cloud-init 可能正在占用 apt 锁。先等它完成:
cloud-init status --wait
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y ca-certificates curl openssl ufw jq dnsutils
如果系统没有 cloud-init,第一条命令报找不到可以跳过。不要看到 apt lock 就直接删除锁文件。
建立非 root 管理用户
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo cp "$HOME/.ssh/authorized_keys" /home/deploy/.ssh/authorized_keys
sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys
sudo chmod 600 /home/deploy/.ssh/authorized_keys
先另开一个终端,确认 deploy 能用密钥登录和执行 sudo,再继续收紧 SSH。
新建 /etc/ssh/sshd_config.d/99-hardening.conf:
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin prohibit-password
KbdInteractiveAuthentication no
检查并重载:
sudo sshd -t
sudo systemctl reload ssh
不要在没有备用登录窗口和云控制台的情况下直接关闭 root 或密码登录。
低内存实例增加 swap
先检查:
free -h
swapon --show
确认没有 swap 后再创建:
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
四、安装 Docker Engine 与 Compose
下面以 Ubuntu 为例,使用 Docker 官方 apt 仓库:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
. /etc/os-release
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker version
sudo docker compose version
Debian 要改用 Docker 的 Debian 仓库,不要把 Ubuntu 的源硬套过去。Docker 官方安装页始终是最终依据。
我不会在教程里开放 Docker Remote API。2375/tcp 和 2376/tcp 不应该暴露公网。
五、DNS 与正式 TLS 证书
先确认 A 记录已经指向 VPS:
dig +short node.example.com
正式证书有两条常见路线:
- DNS-01:适合同机 TCP 443 已被 Xray 使用的情况,也是我更推荐的长期方案。
- HTTP-01:首次部署最容易理解,但签发时要临时开放
80/tcp。
下面用 Certbot standalone 演示 HTTP-01。先在云安全组和 UFW 临时放行 80/tcp:
sudo apt-get install -y certbot
sudo ufw allow 80/tcp
sudo certbot certonly --standalone \
-d node.example.com \
-m admin@example.com \
--agree-tos \
--no-eff-email
这条示例走的是 HTTP-01。为了让 Certbot 以后自动续期,云安全组和 UFW 的 80/tcp 必须保持公网可达:
sudo certbot certificates
如果你不愿长期开放 TCP 80,就不要在签发后简单关掉它并继续期待自动续期;应改成 DNS-01,并配置能够无人值守续期的 DNS 插件或 acme.sh DNS API。另一种办法是写完整的续期 pre/post hook,自动同步修改云防火墙,但这通常比 DNS-01 更复杂。
本文后面使用:
/etc/letsencrypt/live/node.example.com/fullchain.pem
/etc/letsencrypt/live/node.example.com/privkey.pem
如果用 DNS-01,请按 DNS 服务商插件或 acme.sh 的官方文档设置。DNS API Token 必须使用最小权限,签发后不能写进配置分享、终端日志或仓库。证书落盘路径也要与后文的容器挂载保持一致。
自签证书只适合短时诊断。客户端的 skip-cert-verify: true 或 insecure 会跳过证书身份校验,不能当作长期“修复”。
六、建立部署目录
sudo install -d -m 700 /opt/personal-proxy
sudo install -d -m 700 /opt/personal-proxy/xray
sudo install -d -m 700 /opt/personal-proxy/hysteria2
sudo install -d -m 700 /opt/personal-proxy/tuic
cd /opt/personal-proxy
最终目录:
/opt/personal-proxy/
├── docker-compose.yml
├── xray/
│ └── config.json
├── hysteria2/
│ └── config.yaml
└── tuic/
└── config.toml
配置文件和目录都包含凭据:
sudo chmod 700 /opt/personal-proxy
sudo chmod 600 /opt/personal-proxy/xray/config.json
sudo chmod 600 /opt/personal-proxy/hysteria2/config.yaml
sudo chmod 600 /opt/personal-proxy/tuic/config.toml
文件还没创建时,最后三条会报不存在,这是正常的,写完配置后再执行一次。
七、配置 VLESS + REALITY
生成 Reality 密钥
先拉取 Xray 官方镜像作为“版本核对与密钥生成”的临时 bootstrap 镜像,再手动读取输出:
sudo docker pull ghcr.io/xtls/xray-core:latest
sudo docker run --rm ghcr.io/xtls/xray-core:latest x25519
把私钥保存为 REALITY_PRIVATE_KEY,对应的公钥保存为 REALITY_PUBLIC_KEY。服务端只需要私钥,客户端只需要公钥。
再确认镜像实际版本:
sudo docker run --rm ghcr.io/xtls/xray-core:latest version
如果输出不是你准备使用的稳定版,先到官方 Releases 选择明确的稳定 tag,再继续。这里的 latest 只用于发现当前稳定镜像,不会直接写进最终 Compose。
选择 Reality target
不要随便抄一个域名。目标站点至少应:
- 支持 TLS 1.3 和 HTTP/2。
- 不发生跳转。
- 从 VPS 能稳定访问。
- 最好与 VPS 位于相近网络或同 ASN。
- 不要选 Apple / iCloud 站点。
- 避免会把失败鉴权流量变成可滥用转发的特殊 CDN 目标。
可以用当前 Xray 检查候选目标:
sudo docker run --rm ghcr.io/xtls/xray-core:latest \
tls ping CHANGE_ME_REALITY_TARGET:443
Xray 配置
新建 /opt/personal-proxy/xray/config.json:
{
"log": {
"loglevel": "warning"
},
"inbounds": [
{
"tag": "vless-reality-in",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "CHANGE_ME_VLESS_UUID",
"flow": "xtls-rprx-vision",
"email": "personal-node"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"show": false,
"target": "CHANGE_ME_REALITY_TARGET:443",
"xver": 0,
"serverNames": [
"CHANGE_ME_REALITY_TARGET"
],
"privateKey": "CHANGE_ME_REALITY_PRIVATE_KEY",
"shortIds": [
"CHANGE_ME_REALITY_SHORT_ID"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
]
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "blocked"
}
],
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"ip": [
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"::1/128",
"fc00::/7",
"fe80::/10"
],
"outboundTag": "blocked"
}
]
}
}
新版本文档把 raw 和 target 作为主字段;我早期实际跑通的配置使用的是别名 tcp 和 dest。Xray v26.3.27 支持本文组合,但跨版本升级前仍要重新执行配置检查和真实客户端握手。
最后的 routing 规则拒绝代理客户端访问 loopback、RFC 1918 私网、IPv4 link-local、IPv6 ULA 和 link-local。尤其是 169.254.169.254 一类云元数据地址,不能通过代理入口暴露;否则凭据一旦泄漏,风险可能从“别人使用出口”升级为云账号凭据泄漏。
八、配置 Hysteria 2
新建 /opt/personal-proxy/hysteria2/config.yaml:
listen: :443
tls:
cert: /etc/letsencrypt/live/node.example.com/fullchain.pem
key: /etc/letsencrypt/live/node.example.com/privkey.pem
auth:
type: password
password: CHANGE_ME_HY2_PASSWORD
acl:
inline:
- "reject(127.0.0.0/8)"
- "reject(::1/128)"
- "reject(10.0.0.0/8)"
- "reject(172.16.0.0/12)"
- "reject(192.168.0.0/16)"
- "reject(169.254.0.0/16)"
- "reject(fc00::/7)"
- "reject(fe80::/10)"
masquerade:
type: proxy
proxy:
url: https://www.example.com/
rewriteHost: true
这里没有配置 masquerade.listenHTTPS。那个选项会额外监听 TCP 443,与同机的 Xray 冲突。
我也没有默认开启 obfs。需要时优先使用 Hysteria 官方长期支持、客户端明确兼容的方式,并确保服务端和客户端参数完全一致。实验字段不应该直接写进面向所有客户端的基础模板。
个人节点通常不需要在服务端硬写夸张的带宽值。客户端带宽也不能高于真实链路能力,否则可能更慢、更抖,还会浪费流量。
ACL 与 Xray 一样阻断本机、私网、link-local 和云元数据网段。规则从上到下匹配;没有命中时使用内置默认出站。
九、配置 TUIC v5
TUIC 协议仓库维护的是规范,并明确说明没有官方实现。本文使用协议仓库列出的活跃实现 Itsusinn/tuic。不要继续照搬旧 tuic-server 1.0.0 教程,也不要把 TUIC v4 的 token 写进 v5 配置。
新建 /opt/personal-proxy/tuic/config.toml:
acl = """
drop localhost
drop private
"""
server = "[::]:8443"
zero_rtt_handshake = false
dual_stack = true
[users]
CHANGE_ME_TUIC_UUID = "CHANGE_ME_TUIC_PASSWORD"
[tls]
certificate = "/etc/letsencrypt/live/node.example.com/fullchain.pem"
private_key = "/etc/letsencrypt/live/node.example.com/privkey.pem"
hostname = "node.example.com"
alpn = ["h3"]
[quic.congestion_control]
controller = "bbr"
zero_rtt_handshake = false 是保守的安全默认值。客户端对应的 reduce-rtt 也应为 false。ALPN 必须两端一致;服务端写 h3,客户端也写 h3。
TUIC v1.8.10 的 localhost 和 private 已覆盖 IPv4/IPv6 loopback、RFC 1918、169.254.0.0/16、fc00::/7 和 fe80::/10。这里必须写 drop;写成一个没有定义的 reject 出站名,可能回落到默认直连,等于没有阻断。
我原先的节点使用旧 TUIC 本地二进制、JSON 配置和自建镜像;这一节是依据当前维护实现做的现代化,不是旧目录的逐字复刻。如果当前实现的配置格式发生变化,可以用固定版本镜像的 --init 在临时目录生成同版本模板,再把上面的值迁进去,而不是猜字段。
十、编写 Docker Compose
先拉取三个候选镜像,核对容器内版本:
sudo docker pull ghcr.io/xtls/xray-core:latest
sudo docker pull tobyxdd/hysteria:latest
sudo docker pull ghcr.io/itsusinn/tuic-server:latest
sudo docker run --rm ghcr.io/xtls/xray-core:latest version
sudo docker run --rm tobyxdd/hysteria:latest version
sudo docker run --rm ghcr.io/itsusinn/tuic-server:latest --version
如果任一版本不是你准备使用的稳定版,改拉官方 Releases 对应的稳定 tag。确认后记录完整 digest 引用:
sudo docker image inspect ghcr.io/xtls/xray-core:latest \
--format '{{index .RepoDigests 0}}'
sudo docker image inspect tobyxdd/hysteria:latest \
--format '{{index .RepoDigests 0}}'
sudo docker image inspect ghcr.io/itsusinn/tuic-server:latest \
--format '{{index .RepoDigests 0}}'
把三行完整输出分别填到下面的 CHANGE_ME_*_IMAGE_REFERENCE。这样第一次 up 就已经固定到核验过的镜像,不会在未来静默漂移。
新建 /opt/personal-proxy/docker-compose.yml:
services:
xray:
image: CHANGE_ME_XRAY_IMAGE_REFERENCE
container_name: personal-proxy-xray
restart: unless-stopped
volumes:
- ./xray/config.json:/etc/xray/config.json:ro
ports:
- "443:443/tcp"
command: ["run", "-c", "/etc/xray/config.json"]
hysteria2:
image: CHANGE_ME_HY2_IMAGE_REFERENCE
container_name: personal-proxy-hysteria2
restart: unless-stopped
volumes:
- ./hysteria2/config.yaml:/etc/hysteria.yaml:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
ports:
- "443:443/udp"
command: ["server", "-c", "/etc/hysteria.yaml"]
tuic:
image: CHANGE_ME_TUIC_IMAGE_REFERENCE
container_name: personal-proxy-tuic
restart: unless-stopped
volumes:
- ./tuic/config.toml:/etc/tuic/config.toml:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
ports:
- "8443:8443/udp"
command: ["-c", "/etc/tuic/config.toml"]
以后升级时也要先核对新版本、记录新 digest、备份旧 digest,再显式修改 Compose;不要把镜像引用改回 latest。
十一、替换占位符并做配置预检
进入目录:
cd /opt/personal-proxy
sudo grep -R "CHANGE_ME" .
sudo grep -RniE \
'203\.0\.113\.10|198\.51\.100\.25|node\.example\.com|admin@example\.com' \
.
把所有占位符替换成你自己的值,再执行同一条命令。正式启动前它应该没有任何输出。
检查 JSON、TOML 基础语法和 Compose:
sudo jq empty xray/config.json
sudo docker compose config
sudo docker compose run --rm xray \
run -test -c /etc/xray/config.json
TUIC 当前实现没有稳定文档化的独立 --test 命令,不要虚构一个。现有 TOML 以前台启动或容器日志确认解析:
sudo docker compose run --rm tuic --version
如果要用 --init 生成模板,应把固定 digest 镜像的 /root 显式挂载到一个空的临时目录。Compose 里的当前文件只读挂载不会接住生成结果,一次性容器删除后文件也会消失。
十二、云安全组与 UFW
固定办公网、家庭公网或 VPN 出口的用户,服务端口也应优先限制到已知来源 CIDR。只有手机网络等来源经常变化、确实无法白名单时,才考虑对公网开放服务端口,并使用强随机凭据、保持版本更新和日志巡检。
云厂商安全组先配置:
| 协议 | 端口 | 来源 | 用途 |
|---|---|---|---|
| TCP | 22 | ALLOWED_ADMIN_CIDR | SSH |
| TCP | 80 | 0.0.0.0/0 | 仅 HTTP-01 自动续期需要 |
| TCP | 443 | 0.0.0.0/0、需要时 ::/0 | VLESS + REALITY |
| UDP | 443 | 0.0.0.0/0、需要时 ::/0 | Hysteria 2 |
| UDP | 8443 | 0.0.0.0/0、需要时 ::/0 | TUIC |
如果你不使用 IPv6,就不要创建 AAAA 记录,也不要默认放开整套 IPv6 入站。
再配置 UFW:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 198.51.100.25/32 to any port 22 proto tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 8443/udp
sudo ufw enable
sudo ufw status verbose
把 198.51.100.25/32 换成你的管理出口。启用前必须保留当前 SSH 会话,并确认云控制台可用。管理出口变化后,旧 /32 会让 SSH 超时;这时应从云控制台更新安全组和 UFW 来源,而不是重装代理。
我不建议在通用脚本里写 ufw --force reset,因为它会清空服务器已有规则。Docker 发布端口还会写自己的 iptables 规则,因此 UFW 不能代替云安全组;云侧规则才是最外层边界。
最典型的真实故障是:Xray 正常、Hysteria 容器也显示 Up,但客户端一直超时。最后发现云安全组只开了 443/tcp,漏了 443/udp。云平台里 TCP 443 和 UDP 443 必须是两条规则。
十三、启动服务
cd /opt/personal-proxy
sudo chmod 600 xray/config.json hysteria2/config.yaml tuic/config.toml
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 100
确认版本:
sudo docker compose exec xray xray version
sudo docker compose exec hysteria2 hysteria version
sudo docker compose exec tuic tuic-server --version
不同镜像的可执行文件入口可能不同。若最后一条找不到命令,以 docker compose logs tuic 和该镜像官方 README 为准。
确认监听:
sudo ss -lntup | grep -E ':(22|443|8443)\b'
你应该同时看到:
- TCP 443。
- UDP 443。
- UDP 8443。
Test-NetConnection -Port 443 只能证明 TCP 443,不能证明 Hysteria 2 的 UDP 443 正常。
十四、Mihomo 客户端模板
把下面保存成 config.yaml,替换所有 CHANGE_ME:
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
ipv6: false
proxies:
- name: "VPS-VLESS"
type: vless
server: 203.0.113.10
port: 443
uuid: CHANGE_ME_VLESS_UUID
network: tcp
tls: true
udp: true
flow: xtls-rprx-vision
servername: CHANGE_ME_REALITY_TARGET
client-fingerprint: chrome
reality-opts:
public-key: CHANGE_ME_REALITY_PUBLIC_KEY
short-id: CHANGE_ME_REALITY_SHORT_ID
- name: "VPS-HY2"
type: hysteria2
server: 203.0.113.10
port: 443
password: CHANGE_ME_HY2_PASSWORD
sni: node.example.com
skip-cert-verify: false
udp: true
- name: "VPS-TUIC"
type: tuic
server: 203.0.113.10
port: 8443
uuid: CHANGE_ME_TUIC_UUID
password: CHANGE_ME_TUIC_PASSWORD
sni: node.example.com
alpn:
- h3
udp-relay-mode: native
congestion-controller: bbr
reduce-rtt: false
skip-cert-verify: false
proxy-groups:
- name: "PROXY"
type: select
proxies:
- "AUTO"
- "VPS-VLESS"
- "VPS-HY2"
- "VPS-TUIC"
- "DIRECT"
- name: "AUTO"
type: url-test
proxies:
- "VPS-VLESS"
- "VPS-HY2"
url: https://www.gstatic.com/generate_204
interval: 60
tolerance: 120
rules:
- IP-CIDR,203.0.113.10/32,DIRECT,no-resolve
- IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
- IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
- MATCH,PROXY
这里有几个我实际踩过的点:
- Mihomo 侧的 VLESS
network仍写tcp,服务端 Xray 新配置里的 canonical 名称可以是raw。 client-fingerprint要逐节点写,不要依赖已经移除的全局字段。- TUIC v5 使用 UUID + password,不用 v4 的 token。
- TUIC 的
reduce-rtt: false与服务端关闭 0-RTT 对应。 - 管理 VPS 的公网 IP 要放在靠前的
DIRECT规则里,否则 TUN 模式下 SSH 可能被末尾的MATCH,PROXY带回代理。 - 自动测速不必把所有节点都塞进去。稳定节点少一点,通常比八个候选来回抖更好。
用 Mihomo 自带检查:
mihomo -t -f config.yaml
Clash Verge Rev 导入后,还要确认它实际使用的 Mihomo 内核版本支持这些字段。系统代理只覆盖遵从系统代理的应用;需要接管命令行、游戏等程序时才考虑 TUN。
Shadowrocket 可以使用 VLESS Vision、REALITY、Hysteria 2 和 TUIC 的基础字段,但它是闭源客户端,没有完整公开的逐字段兼容矩阵。保持 App Store 正版为当前版本,高级实验特性要单独实测。分享 URI、订阅、二维码和 Base64 都等同于明文凭据,不能公开。
十五、端到端验收
不要以“容器是 Up”作为完成标准。我的验收顺序是:
- 服务端配置能解析。
- 三个容器保持 Up,没有重启循环。
ss同时看到443/tcp、443/udp、8443/udp。- 域名 A 记录解析到目标 VPS。
- 证书的 SAN、有效期和 SNI 匹配。
- Mihomo 配置通过
-t检查。 - 分别手选 VLESS、Hysteria 2、TUIC。
- 每条线路都查询一次出口 IP。
- 打开实际常用站点,验证 HTTP、长连接和下载。
- 重启 VPS 后再测一遍,确认 Docker 与容器自动拉起。
本地 Mihomo 的 mixed port 为 7890 时,可以这样看出口:
curl -x http://127.0.0.1:7890 https://api.ipify.org
切换节点后重复运行,结果应是目标 VPS 的公网出口。Hysteria 2 和 TUIC 还要结合客户端握手日志和实际 UDP 应用验证;单纯 TCP 端口探测没有意义。
服务端同步看日志:
cd /opt/personal-proxy
sudo docker compose logs -f xray
sudo docker compose logs -f hysteria2
sudo docker compose logs -f tuic
十六、证书自动续期
Certbot 会定期尝试续期,但服务进程还要重新读取新证书。创建 deploy hook,在续期成功后重启 Hysteria 2 和 TUIC:
sudo install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/restart-personal-proxy.sh
内容:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/personal-proxy
/usr/bin/docker compose restart hysteria2 tuic
然后:
sudo chmod 750 /etc/letsencrypt/renewal-hooks/deploy/restart-personal-proxy.sh
sudo certbot renew --dry-run
如果使用 HTTP-01,续期时 TCP 80 必须能从公网到达;如果不想长期开放 80,就改用可自动续期的 DNS-01。DNS-01 使用 acme.sh 时,也要配置相应 deploy hook,把新证书复制到容器挂载路径并重启 Hysteria 2 与 TUIC。
十七、日常巡检、升级与回滚
日常巡检
cd /opt/personal-proxy
sudo docker compose ps
sudo docker compose logs --tail 100
sudo ufw status verbose
sudo ss -lntup
sudo systemctl is-active docker
sudo certbot certificates
还应在客户端做一次真实出口检查。服务端看起来正常,不等于云防火墙、运营商 UDP 或客户端配置正常。
备份
sudo tar -C / -czf "/root/personal-proxy-$(date +%F).tgz" \
opt/personal-proxy \
etc/letsencrypt
sudo chmod 600 /root/personal-proxy-*.tgz
这个压缩包含全部节点凭据和证书私钥。离开服务器前要再次加密,不能上传到公开网盘、Git 仓库、博客附件,也不能把整个备份包交给不受信任的分析工具。
升级
cd /opt/personal-proxy
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 100
升级前记录:
- 当前配置备份。
- 三个镜像 digest。
- 三个服务版本。
- 一份可用的客户端配置。
升级后完整重复端到端验收。不要因为某个 GitHub tag 更新,就直接把稳定生产节点切到 Pre-release。
回滚
回滚不是“再拉一次 latest”,而是把 Compose 中的镜像恢复到之前记录的 tag 或 digest,然后:
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs --tail 100
配置格式也可能随版本变化,所以镜像和配置必须成对备份。
十八、常见故障矩阵
| 症状 | 优先检查 | 常见原因 |
|---|---|---|
| 容器 Up,但 TCP 443 没监听 | docker compose logs xray、配置挂载路径 | Xray 读的不是你改的文件;原生安装常见正确路径是 /usr/local/etc/xray/ |
| VLESS 握手失败 | UUID、公钥、short ID、serverName、target | Reality 参数有一项不一致 |
| Hysteria 2 一直 timeout | 云安全组与 UFW 的 443/udp | 只开放了 TCP 443 |
| TUIC 一直 timeout | 8443/udp、UUID、密码、ALPN | UDP 端口没放行或 ALPN 不一致 |
| x509 / certificate 错误 | 域名、SNI、证书链、有效期 | 不要把 skip-cert-verify 当永久修复 |
| 私钥 permission denied | 文件 owner、group、mode、容器挂载 | 私钥对服务用户不可读 |
| 服务反复重启 | docker compose logs --tail 100 | JSON/YAML/TOML 字段或缩进错误 |
| apt 被锁 | cloud-init status --wait | 新机初始化仍在运行 |
| 1 GB 机器偶发进程消失 | dmesg -T 后筛选 oom、free -h | 内存不足且没有 swap |
| 延迟始终 200 ms 以上 | 到机房的物理 RTT | 换协议不能突破物理距离 |
| SSH 开 TUN 后断开 | Mihomo 规则顺序 | VPS 管理 IP 被 MATCH,PROXY 捕获 |
| Windows 拒绝 PEM 私钥 | icacls 查看文件 ACL | 私钥继承了其他用户可读权限 |
| SSH 突然超时 | 云安全组 SSH 来源、当前公网 IP | 管理出口变化后旧 /32 不再匹配 |
| 三条线路同时离线 | 云控制台里的 VM 电源状态 | Spot/可抢占实例被驱逐或主机已停止 |
| 迁移后仍连到旧机器 | 客户端 server、DNS 缓存 | 客户端配置或 A/AAAA 仍保留旧 IP |
| 重启后节点失联 | Docker 是否 enabled、容器 restart policy | 服务未设自启动 |
| 有 AAAA 时偶发失败 | IPv6 监听与两层防火墙 | IPv6 路径配置不完整 |
Xray 原生安装时可用:
xray run -test -c /usr/local/etc/xray/config.json
systemctl status xray --no-pager -l
journalctl -u xray -e --no-pager
Hysteria 2 原生安装时可用:
systemctl status hysteria-server.service --no-pager -l
journalctl -u hysteria-server.service -e --no-pager
不要根据一个旧教程假设服务名和配置路径,先看 systemd unit 的 ExecStart。
十九、多节点与链式代理
多节点配置里,我更倾向于:
AUTO只放长期稳定、延迟差距合理的两三个节点。- Spot 节点和 TUIC 备用线路保留手选。
- 地区组与业务组分开,便于看清最终出口。
- 所有 VPS 管理 IP 前置
DIRECT。
多跳时,最后一跳决定公网出口。Mihomo 旧的 relay 已被移除,当前应使用代理副本配 dialer-proxy。这类配置会显著增加排错复杂度,而且 UDP 不适合作为多跳主线;单节点没验收完之前不要上链式代理。
二十、发布或分享前的脱敏检查
下面这些都属于凭据:
- 服务器 IP 和真实域名。
- UUID、密码、Reality private key / public key / short ID。
- TLS 私钥。
- Cloudflare 或其他 DNS API Token。
- VLESS、Hysteria 2、TUIC 分享链接。
- Mihomo / Clash / FlClash 完整配置。
- Base64 订阅、二维码、Shadowrocket 订阅 URL。
- 带串口输出的 cloud-init 日志。
- 完整服务器备份包。
发布文档前可以先扫描:
grep -RniE \
'password|private.?key|public.?key|short.?id|token|uuid|hysteria2://|vless://|tuic://' \
.
扫描命中不一定都是泄漏,但每一处都要人工确认。半脱敏也不够安全:不要保留 IP 前两段、UUID 前后几位或真实域名前缀。
二十一、最终上线检查清单
- SSH 密钥登录已在第二个窗口验证。
- 云安全组只开放必要端口。
- UFW 没有清掉服务器原有必要规则。
-
443/tcp、443/udp、8443/udp分别正确。 - DNS 为直连源站,不走普通 CDN。
- 正式证书有效,客户端没有跳过验证。
- Reality 私钥只在服务端,公钥只在客户端。
-
grep -R CHANGE_ME没有输出。 - 三个容器都稳定运行。
- 三条线路分别验证了真实出口。
- 重启 VPS 后服务自动恢复。
- 备份已经加密并保留在安全位置。
- 当前版本和镜像 digest 已记录。
完成这张清单,才算真正部署完。一个可维护的节点,不是“今天能连上”就结束,而是几个月后忘了细节,仍然能根据目录、日志、版本和备份把它恢复出来。
官方资料
- Xray-core Releases:https://github.com/XTLS/Xray-core/releases
- Xray 官方安装脚本:https://github.com/XTLS/Xray-install
- Xray VLESS:https://xtls.github.io/config/inbounds/vless.html
- Xray REALITY:https://xtls.github.io/en/config/transports/reality.html
- Xray REALITY 示例:https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-Vision-REALITY
- Hysteria 2 安装:https://v2.hysteria.network/docs/getting-started/Installation/
- Hysteria 2 服务端配置:https://v2.hysteria.network/docs/advanced/Full-Server-Config/
- Hysteria 2 排障:https://v2.hysteria.network/docs/advanced/Troubleshooting/
- TUIC v5 规范与实现列表:https://github.com/tuic-protocol/tuic
- Itsusinn/tuic 服务端:https://github.com/Itsusinn/tuic/tree/main/tuic-server
- Mihomo VLESS:https://wiki.metacubex.one/en/config/proxies/vless/
- Mihomo Hysteria 2:https://wiki.metacubex.one/en/config/proxies/hysteria2/
- Mihomo TUIC:https://wiki.metacubex.one/en/config/proxies/tuic/
- Clash Verge Rev 安装:https://www.clashverge.dev/install.html
- Shadowrocket App Store:https://apps.apple.com/us/app/shadowrocket/id932747118
- Docker Engine Ubuntu 安装:https://docs.docker.com/engine/install/ubuntu/