返回博客

VPS 三协议节点完整部署:VLESS Reality、Hysteria 2 与 TUIC

从 VPS 初始化与 SSH 加固,到 Docker Compose 部署、Mihomo 接入、验收、排错、备份和回滚的一套可维护实践。

VPSVLESS RealityHysteria 2TUICDockerMihomo

过去一段时间,我在 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 或其他会伤害第三方的行为。

阅读路线

这是一篇从准备到运维的长文,可以按五段阅读:

  1. “一”到“六”:VPS、变量、SSH、Docker、证书和目录准备。
  2. “七”到“九”:VLESS Reality、Hysteria 2、TUIC 三个服务端配置。
  3. “十”到“十三”:Compose、占位符检查、防火墙和启动。
  4. “十四”到“十五”:Mihomo 客户端和端到端验收。
  5. “十六”以后:续期、备份、升级、回滚、排错和多节点。

第一次部署做到 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 + REALITYRAW/TCP443/tcp默认主线路,兼容性好
Hysteria 2QUIC/UDP443/udp丢包环境下的备用或主线路
TUIC v5QUIC/UDP8443/udp第二条 UDP 备用线路

如果是第一次部署,可以先完成 VLESS 和 Hysteria 2,验收通过以后再加 TUIC。这样排错会简单很多。

本文核验环境与版本

版本变化很快。下面是我在 2026-07-12 写这篇文章时核对的稳定版本,不代表以后永远应该锁死在这些版本:

组件核验时的稳定版本说明
Xray-corev26.3.27v26.7.11 当时仍是 Pre-release
Hysteria 2v2.9.3已包含 v2.9.2 的重要安全修复
TUIC协议 v5;Itsusinn 实现 v1.8.10TUIC 协议仓库明确没有“官方实现”
Mihomov1.19.28客户端配置按这一代字段核验
Clash Verge Revv2.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_IP203.0.113.10,必须替换
DOMAINnode.example.com,必须替换
SSH_USER非 root 管理用户
ALLOWED_ADMIN_CIDR你的管理出口,例如 198.51.100.25/32
VLESS_UUIDVLESS 用户 UUID
REALITY_TARGET符合要求的 TLS 站点
REALITY_PRIVATE_KEY只保存在服务端
REALITY_PUBLIC_KEY只给客户端
REALITY_SHORT_ID0–16 位、偶数长度的十六进制字符串
HY2_PASSWORDHysteria 2 强随机密码
TUIC_UUIDTUIC v5 用户 UUID
TUIC_PASSWORDTUIC 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/tcp2376/tcp 不应该暴露公网。

五、DNS 与正式 TLS 证书

先确认 A 记录已经指向 VPS:

dig +short node.example.com

正式证书有两条常见路线:

  1. DNS-01:适合同机 TCP 443 已被 Xray 使用的情况,也是我更推荐的长期方案。
  2. 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: trueinsecure 会跳过证书身份校验,不能当作长期“修复”。

六、建立部署目录

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"
      }
    ]
  }
}

新版本文档把 rawtarget 作为主字段;我早期实际跑通的配置使用的是别名 tcpdest。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 的 localhostprivate 已覆盖 IPv4/IPv6 loopback、RFC 1918、169.254.0.0/16fc00::/7fe80::/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。只有手机网络等来源经常变化、确实无法白名单时,才考虑对公网开放服务端口,并使用强随机凭据、保持版本更新和日志巡检。

云厂商安全组先配置:

协议端口来源用途
TCP22ALLOWED_ADMIN_CIDRSSH
TCP800.0.0.0/0仅 HTTP-01 自动续期需要
TCP4430.0.0.0/0、需要时 ::/0VLESS + REALITY
UDP4430.0.0.0/0、需要时 ::/0Hysteria 2
UDP84430.0.0.0/0、需要时 ::/0TUIC

如果你不使用 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”作为完成标准。我的验收顺序是:

  1. 服务端配置能解析。
  2. 三个容器保持 Up,没有重启循环。
  3. ss 同时看到 443/tcp443/udp8443/udp
  4. 域名 A 记录解析到目标 VPS。
  5. 证书的 SAN、有效期和 SNI 匹配。
  6. Mihomo 配置通过 -t 检查。
  7. 分别手选 VLESS、Hysteria 2、TUIC。
  8. 每条线路都查询一次出口 IP。
  9. 打开实际常用站点,验证 HTTP、长连接和下载。
  10. 重启 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、targetReality 参数有一项不一致
Hysteria 2 一直 timeout云安全组与 UFW 的 443/udp只开放了 TCP 443
TUIC 一直 timeout8443/udp、UUID、密码、ALPNUDP 端口没放行或 ALPN 不一致
x509 / certificate 错误域名、SNI、证书链、有效期不要把 skip-cert-verify 当永久修复
私钥 permission denied文件 owner、group、mode、容器挂载私钥对服务用户不可读
服务反复重启docker compose logs --tail 100JSON/YAML/TOML 字段或缩进错误
apt 被锁cloud-init status --wait新机初始化仍在运行
1 GB 机器偶发进程消失dmesg -T 后筛选 oomfree -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/tcp443/udp8443/udp 分别正确。
  • DNS 为直连源站,不走普通 CDN。
  • 正式证书有效,客户端没有跳过验证。
  • Reality 私钥只在服务端,公钥只在客户端。
  • grep -R CHANGE_ME 没有输出。
  • 三个容器都稳定运行。
  • 三条线路分别验证了真实出口。
  • 重启 VPS 后服务自动恢复。
  • 备份已经加密并保留在安全位置。
  • 当前版本和镜像 digest 已记录。

完成这张清单,才算真正部署完。一个可维护的节点,不是“今天能连上”就结束,而是几个月后忘了细节,仍然能根据目录、日志、版本和备份把它恢复出来。

官方资料