Matrix Synapse 接入 LiveKit:Cloudflare Tunnel 免 NPM Docker Compose 部署

整理一套不经过 Nginx Proxy Manager 的 MatrixRTC / LiveKit / JWT / coturn 部署方案:Cloudflare Tunnel 转发 HTTP/WebSocket,LiveKit、JWT、coturn 使用 host 网络;适合没有公网 443 的环境,但在中国大陆网络下不一定适合作为 LiveKit 呼叫主方案。

这篇记录一套已经实际跑通的 Matrix Synapse 接入 LiveKit 语音视频方案:公网 HTTPS 入口走 Cloudflare Tunnel,Docker 主机上的 LiveKit server、matrix-livekit-jwt-service、coturn 都使用 network_mode: host,全程不走 Nginx Proxy Manager。

先把限制说在前面:如果主要使用地点在中国大陆,这套方案不一定稳定,尤其不适合把 LiveKit 呼叫控制面长期压在 Cloudflare Tunnel 上。实际现象可能是 coturn 老通话路径正常,但使用 LiveKit / MatrixRTC 呼叫时,Element 点呼叫后白屏、等待 8 秒甚至更久才弹出呼叫界面。这个问题通常不是 coturn relay 端口的问题,而是 LiveKit 相关的 HTTPS / WebSocket / JWT 控制链路经 Tunnel 后延迟和抖动变大。

如果你有稳定公网 80/443,优先看“公网 443 标准部署方案”。如果没有本地标准 443,但能转发自定义 HTTPS 端口并想继续用 NPM,优先看“Cloudflare Worker 与 NPM 部署方案”。本文更适合作为没有公网 443 时的临时方案、验证方案,或者对呼叫启动延迟不敏感的环境。

这不是 Synapse 从零安装篇,而是在已经有 Matrix Synapse 的基础上补齐新版 Element / MatrixRTC 需要的 LiveKit 后端。本文示例里:

Matrix server_name: 666666.xyz
Docker 主机内网 IP: 10.20.20.8
Synapse HTTP:       10.20.20.8:8008
well-known:         10.20.20.8:8899
LiveKit API/WS:     10.20.20.8:7880
JWT service:        10.20.20.8:8170
ntfy:               10.20.20.8:80
coturn:             10.20.20.8:3478

实际部署时,把 666666.xyz10.20.20.8、所有密钥都换成自己的。不要把真实的 LiveKit secret、Cloudflare Tunnel token、Synapse TURN shared secret 写进公开文章或截图里。

本文里的 Compose 写法按当前已经跑通的 Docker 主机配置整理。原机器上有些服务最初是用 创建容器.sh 跑起来的,例如 cloudflaredlivekit-servermatrix-livekit-jwt-service;下面统一改写成等价 docker-compose.yaml,后续重启、迁移和排障会清楚很多。

一、整体架构

最终访问路径是这样:

Element 客户端
  |
  | HTTPS / WebSocket
  v
Cloudflare Tunnel
  |
  | 内网 HTTP 转发
  v
Docker 主机 10.20.20.8
  |-- 8899 -> matrix-well-known-web
  |-- 8008 -> matrix-synapse
  |-- 8170 -> matrix-livekit-jwt-service
  |-- 7880 -> livekit-server
  |-- 80   -> ntfy

LiveKit RTC 媒体端口:
  50201-50400/udp
  17882/tcp  可选,LiveKit TCP fallback

coturn:
  3478/tcp
  3478/udp
  50000-50200/udp

Cloudflare Tunnel 负责 HTTP 和 WebSocket,也就是:

  • /.well-known/matrix/client
  • Synapse Client API
  • LiveKit 7880 API / WebSocket
  • JWT service 8170
  • ntfy 80 Web UI / API

但是 Tunnel 不能替代普通 UDP 媒体端口。LiveKit 的 50201-50400/udp、coturn 的 3478/udp 和 relay 端口仍然要在路由器、光猫或防火墙上放行/转发到 Docker 主机。

二、Cloudflare Tunnel 路由

先在 Cloudflare Zero Trust 里创建 Tunnel:

Zero Trust -> Networks -> Tunnels -> Create a tunnel

Connector 选择 Cloudflared,运行环境选择 Docker,Cloudflare 会生成一条带 token 的命令。推荐整理成 compose 管理。

创建目录:

mkdir -p /root/data/docker_data/cloudflared
cd /root/data/docker_data/cloudflared

创建 /root/data/docker_data/cloudflared/docker-compose.yaml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    network_mode: host
    command:
      - tunnel
      - --protocol
      - http2
      - --region
      - us
      - --no-autoupdate
      - run
      - --token
      - "替换成你的 Cloudflare Tunnel token"

启动:

docker compose up -d
docker logs -f cloudflared

回到 Cloudflare Tunnel 页面,Connector 状态变成 Healthy 后,添加 Public Hostname。

本文使用一个主域名和几个子域名。当前 Cloudflare Tunnel 的已发布应用程序路由是:

Hostname Path Type URL
666666.xyz /.well-known HTTP 10.20.20.8:8899
666666.xyz * HTTP 10.20.20.8:8008
livekit.666666.xyz 留空或 * HTTP 10.20.20.8:7880
matrix-jwt.666666.xyz 留空或 * HTTP 10.20.20.8:8170
sms.666666.xyz 留空或 * HTTP 10.20.20.8:80

顺序很重要。666666.xyz/.well-known 要排在 * 前面,否则 .well-known 请求会被转发到 Synapse,而不是独立的 well-known 容器。

这里的 cloudflared 明确使用了 --protocol http2 --region us,这是当前实际跑通配置。QUIC 在部分家庭网络里可能会抖,HTTP/2 通常更稳。

Cloudflare Tunnel Public Hostnames 映射示意图

三、端口和 DNS

推荐这样规划:

记录 用途 Cloudflare 状态
666666.xyz Matrix homeserver;Tunnel 转发 /.well-known 和 Synapse 8008 Tunnel 自动创建,橙云/CNAME 到 Tunnel
livekit.666666.xyz LiveKit API/WebSocket;Tunnel 转发到 7880 Tunnel 自动创建,橙云/CNAME 到 Tunnel
matrix-jwt.666666.xyz matrix-livekit-jwt-service;Tunnel 转发到 8170 Tunnel 自动创建,橙云/CNAME 到 Tunnel
sms.666666.xyz ntfy 通知服务;Tunnel 转发到 ntfy 的 80 Tunnel 自动创建,橙云/CNAME 到 Tunnel
coturncoturn7829.666666.xyz coturn TURN DNS only,灰云,直接指向公网 IP

coturn 的域名可以托管在 Cloudflare DNS,但 coturn 流量不能走 Cloudflare Tunnel,也不能开橙云代理。TURN 是 UDP/TCP 中继服务,必须使用 DNS only,也就是灰云,让客户端直接连到你的公网 IP:

coturncoturn7829.666666.xyz -> 你的公网 IP

如果家宽公网 IP 会变化,可以继续用 ddns-go、脚本或 Cloudflare API 更新 Cloudflare 里的这条 DNS only 记录。不要把它配置成 Tunnel Public Hostname,也不要把小云朵打开;否则手机端、外网客户端很容易出现能呼叫但没声音、没画面的问题。

路由器或防火墙至少放行/转发 LiveKit 和 coturn 的 UDP 媒体端口。5 人左右的小规模使用,不需要给 LiveKit 预留过大的 UDP 端口段;这里把 coturn relay 放在 50000-50200/udp,LiveKit RTC 放在 50201-50400/udp,两段连续且不重叠。RouterOS 防火墙或端口转发里可以直接写一条 50000-50400/udp 覆盖这两段。

17882/tcp 是 TCP fallback,当前实测通话走的是 UDP,不转发它也可以正常呼叫;如果希望在部分 UDP 受限网络里多一个兜底,再额外转发它。

LiveKit:
  50201-50400/udp  必需
  17882/tcp        可选,TCP fallback

coturn:
  3478/tcp
  3478/udp
  50000-50200/udp

如果公网 IPv6 不稳定,coturn 域名先只保留 A 记录,避免手机优先走 IPv6 后媒体不通。

四、LiveKit server

创建目录:

mkdir -p /root/data/docker_data/livekit-server/etc
cd /root/data/docker_data/livekit-server

生成 LiveKit secret:

openssl rand -base64 32

编辑 /root/data/docker_data/livekit-server/etc/livekit.yaml

port: 7880
bind_addresses:
  - ""

rtc:
  tcp_port: 17882
  port_range_start: 50201
  port_range_end: 50400
  use_external_ip: true
  enable_loopback_candidate: false
  stun_servers:
    - "stun.miwifi.com:3478"
    - "stun.chat.bilibili.com:3478"

keys:
  livekit_api_key_example: livekit_secret_example

说明:

  • port: 7880 是 LiveKit API 和 WebSocket 入口,Cloudflare Tunnel 转发到这里。
  • 50201-50400/udp 是 LiveKit RTC UDP 端口段。5 人左右使用时,200 个 UDP 端口已经很宽裕;如果后面用户数明显增加,再扩大这段范围。
  • tcp_port: 17882 是 LiveKit RTC TCP fallback 端口。它不是当前通话成功的必需项,不转发也能用;只是在遇到 UDP 被限制的网络时可以作为备用。
  • use_external_ip: true 会让 LiveKit 自动探测外网 IP,适合家宽/内网主机端口转发场景。
  • keys 里的 key/secret 要和 JWT 服务完全一致。

创建 /root/data/docker_data/livekit-server/docker-compose.yaml

services:
  livekit-server:
    image: livekit/livekit-server:latest
    container_name: livekit-server-test
    restart: unless-stopped
    network_mode: host
    command: --config /etc/livekit.yaml
    volumes:
      - ./etc/livekit.yaml:/etc/livekit.yaml:ro

启动:

docker compose up -d
docker logs -f livekit-server-test

如果日志里能看到监听 7880,并且后面发起呼叫时出现 participant activemediaTrack published,说明 LiveKit 已经工作。日志里的 connectionType: udp 表示这次通话走的是 UDP 媒体端口,不依赖 17882/tcp

五、matrix-livekit-jwt-service

JWT 服务负责把 Matrix 用户身份换成 LiveKit token。它也使用 host 网络,监听宿主机 8170

创建目录:

mkdir -p /root/data/docker_data/matrix-livekit-jwt-service
cd /root/data/docker_data/matrix-livekit-jwt-service

创建 /root/data/docker_data/matrix-livekit-jwt-service/docker-compose.yaml

services:
  matrix-livekit-jwt-service:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    container_name: matrix-livekit-jwt-service
    restart: unless-stopped
    network_mode: host
    extra_hosts:
      - "666666.xyz:127.0.0.1"
    environment:
      LIVEKIT_JWT_BIND: ":8170"
      LIVEKIT_URL: "wss://livekit.666666.xyz"
      LIVEKIT_KEY: "livekit_api_key_example"
      LIVEKIT_SECRET: "livekit_secret_example"
      LIVEKIT_FULL_ACCESS_HOMESERVERS: "666666.xyz"
      LIVEKIT_INSECURE_SKIP_VERIFY_TLS: "YES_I_KNOW_WHAT_I_AM_DOING"

关键点:

  • LIVEKIT_URL 必须是客户端能访问的公网地址,所以这里写 wss://livekit.666666.xyz,不是 http://127.0.0.1:7880
  • LIVEKIT_KEYLIVEKIT_SECRET 必须和 livekit.yaml 一致。
  • LIVEKIT_FULL_ACCESS_HOMESERVERS 写 Matrix 用户 ID 的 server_name,例如 @black:666666.xyz 后面的 666666.xyz
  • extra_hosts 只影响这个 JWT 容器,让它校验 homeserver 时走第六节的本机代理,不影响外部客户端和 Cloudflare Tunnel。
  • LIVEKIT_INSECURE_SKIP_VERIFY_TLS 是配合本机自签 HTTPS 代理使用的。如果你不用第六节的本机校验加速,而是让 JWT 直接走公网正式证书,可以去掉这一行。

启动:

docker compose up -d
docker logs -f matrix-livekit-jwt-service

基础检查:

curl -i http://127.0.0.1:8170/healthz
curl -i https://matrix-jwt.666666.xyz/healthz

都返回 200 才继续。

六、JWT 本机校验加速

如果 JWT 服务校验 Matrix OpenID 时走 https://666666.xyz,在 Tunnel 方案里可能会出现这种绕路:

JWT 容器 -> Cloudflare -> Tunnel -> Synapse

实际测试这个链路一次可能要 0.8-1.1s,一次呼叫里又可能校验多次,于是 Element 发起呼叫时会白屏等待几秒。

可以加一个只给本机用的 HTTPS 反代,把 JWT 的 homeserver 校验改成本机直连 Synapse。这个容器只监听 127.0.0.1:443,不占公网 443,不影响 Cloudflare Tunnel。

创建目录:

mkdir -p /root/data/docker_data/matrix-local-federation-proxy/conf
mkdir -p /root/data/docker_data/matrix-local-federation-proxy/certs
cd /root/data/docker_data/matrix-local-federation-proxy

生成本机自签证书:

openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
  -subj '/CN=666666.xyz' \
  -keyout ./certs/local.key \
  -out ./certs/local.crt

创建 /root/data/docker_data/matrix-local-federation-proxy/conf/default.conf

server {
    listen 127.0.0.1:443 ssl;
    server_name 666666.xyz;

    ssl_certificate /etc/nginx/certs/local.crt;
    ssl_certificate_key /etc/nginx/certs/local.key;

    location = /.well-known/matrix/server {
        default_type application/json;
        return 200 '{"m.server":"666666.xyz:443"}';
    }

    location /_matrix/federation/ {
        proxy_pass http://127.0.0.1:8448;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
    }
}

创建 /root/data/docker_data/matrix-local-federation-proxy/docker-compose.yaml

services:
  matrix-local-federation-proxy:
    image: nginx:alpine
    container_name: matrix-local-federation-proxy
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro

启动:

docker compose up -d

测试本机代理:

curl -k -i --resolve 666666.xyz:443:127.0.0.1 \
  'https://666666.xyz/_matrix/federation/v1/openid/userinfo?access_token=test'

返回 401 是正常的,因为这里用的是假 token。重点是响应要很快,并且不是连接失败。

如果第五节已经按完整 Compose 写入 extra_hostsLIVEKIT_INSECURE_SKIP_VERIFY_TLS,这里不用再改。最终 JWT Compose 应包含下面这两项:

services:
  matrix-livekit-jwt-service:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    container_name: matrix-livekit-jwt-service
    restart: unless-stopped
    network_mode: host
    extra_hosts:
      - "666666.xyz:127.0.0.1"
    environment:
      LIVEKIT_JWT_BIND: ":8170"
      LIVEKIT_URL: "wss://livekit.666666.xyz"
      LIVEKIT_KEY: "livekit_api_key_example"
      LIVEKIT_SECRET: "livekit_secret_example"
      LIVEKIT_FULL_ACCESS_HOMESERVERS: "666666.xyz"
      LIVEKIT_INSECURE_SKIP_VERIFY_TLS: "YES_I_KNOW_WHAT_I_AM_DOING"

重启 JWT:

cd /root/data/docker_data/matrix-livekit-jwt-service
docker compose up -d

这一步不是必须,但很推荐。它能把 JWT 校验用户身份时的内网绕路从“出门走 Cloudflare 再回来”变成“本机直连 Synapse”。

七、Synapse 配置

Synapse 这里假设已经在运行,只补充 LiveKit 需要的配置。

1. docker-compose 端口

编辑 Synapse 的 compose,例如:

nvim /root/data/docker_data/synapse/docker-compose.yml

确认 Synapse 至少暴露 8008,并额外给本机 federation 校验开一个只监听 127.0.0.18448

services:
  synapse:
    image: matrixdotorg/synapse:latest
    container_name: matrix-synapse
    restart: unless-stopped
    ports:
      - "8008:8008"
      - "127.0.0.1:8448:8448"
    volumes:
      - ./data:/data

8008 给 Cloudflare Tunnel 转发客户端请求,127.0.0.1:8448 只给上一节的本机 federation proxy 使用,不暴露到公网。

2. homeserver.yaml listener

编辑:

nvim /root/data/docker_data/synapse/data/homeserver.yaml

确认有 8008 listener:

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    resources:
      - names: [client, federation]
        compress: false

再加一个只给本机代理用的 8448 listener:

  - port: 8448
    tls: false
    type: http
    bind_addresses: ["0.0.0.0"]
    resources:
      - names: [federation]
        compress: false

虽然这里容器内监听 0.0.0.0:8448,但 compose 只映射到宿主机 127.0.0.1:8448,外网访问不到。

3. MatrixRTC 配置

homeserver.yaml 里加入:

experimental_features:
  msc4143_enabled: true

matrix_rtc:
  transports:
    - type: livekit
      livekit_service_url: "https://matrix-jwt.666666.xyz"

如果原来已经有 experimental_features,不要重复写第二段,把 msc4143_enabled: true 合并进去。

4. coturn 配置保留

旧客户端和部分 NAT 场景仍然需要 coturn。Synapse 里继续保留 TURN 配置:

turn_uris:
  - "turn:coturncoturn7829.666666.xyz?transport=udp"
  - "turn:coturncoturn7829.666666.xyz?transport=tcp"
turn_shared_secret: "替换成你的 TURN shared secret"
turn_user_lifetime: 86400000
turn_allow_guests: True

修改后重启:

cd /root/data/docker_data/synapse
docker compose up -d
docker logs -f matrix-synapse

检查本机 federation endpoint:

curl -i http://127.0.0.1:8448/_matrix/federation/v1/version
curl -i 'http://127.0.0.1:8448/_matrix/federation/v1/openid/userinfo?access_token=test'

第一个应返回 200,第二个用假 token 返回 401 是正常的。

八、well-known 容器

Cloudflare Tunnel 里 666666.xyz/.well-known 会转发到这个 Nginx 容器。它负责返回 Matrix 客户端发现需要的 JSON。

创建目录:

mkdir -p /root/data/docker_data/matrix-well-known/web/.well-known/matrix
mkdir -p /root/data/docker_data/matrix-well-known/config
cd /root/data/docker_data/matrix-well-known

创建 /root/data/docker_data/matrix-well-known/web/.well-known/matrix/client

{
  "m.homeserver": {
    "base_url": "https://666666.xyz"
  },
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://matrix-jwt.666666.xyz"
    }
  ],
  "io.element.rtc": {
    "livekit": {
      "service_url": "https://matrix-jwt.666666.xyz"
    }
  }
}

创建 /root/data/docker_data/matrix-well-known/web/.well-known/matrix/server

{
  "m.server": "666666.xyz:443"
}

创建 /root/data/docker_data/matrix-well-known/config/default.conf

server {
    listen 80;
    server_name localhost;

    location /.well-known/matrix/client {
        alias /usr/share/nginx/html/.well-known/matrix/client;
        default_type application/json;
        add_header Access-Control-Allow-Origin * always;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }

    location /.well-known/matrix/server {
        alias /usr/share/nginx/html/.well-known/matrix/server;
        default_type application/json;
        add_header Access-Control-Allow-Origin * always;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }

    location / {
        return 404;
    }
}

创建 /root/data/docker_data/matrix-well-known/docker-compose.yaml

services:
  matrix-well-known-web:
    image: nginx:alpine
    container_name: matrix-well-known-web
    restart: unless-stopped
    ports:
      - "8899:80"
    volumes:
      - ./web:/usr/share/nginx/html:ro
      - ./config/default.conf:/etc/nginx/conf.d/default.conf:ro

启动:

docker compose up -d

验证:

curl -i http://127.0.0.1:8899/.well-known/matrix/client
curl -i https://666666.xyz/.well-known/matrix/client
curl -i https://666666.xyz/.well-known/matrix/server

重点看:

HTTP/2 200
content-type: application/json
access-control-allow-origin: *

如果修改了 JSON 后客户端不生效,先退出 Element 重新登录,或者清掉客户端缓存。这里已经加了 no-cache 头,避免 Cloudflare 和浏览器长时间缓存旧配置。

九、coturn

coturn 建议继续保留,尤其是旧版 Element、Element Classic 或复杂 NAT 场景。它和 LiveKit 不冲突。

创建目录:

mkdir -p /root/data/docker_data/coturn/etc/coturn
cd /root/data/docker_data/coturn

先生成一个 TURN shared secret,复制输出结果,后面 turnserver.conf 和 Synapse 的 turn_shared_secret 都用同一个值:

openssl rand -hex 34

创建 /root/data/docker_data/coturn/etc/coturn/turnserver.conf。下面按当前系统里跑通的配置整理,只把 shared secret 和公网 IP 改成占位符:

# 排障时可以临时打开 verbose,稳定后建议注释掉,避免日志太多
# verbose

# 启用长期凭证与共享密钥认证
use-auth-secret
static-auth-secret=替换成你的 TURN shared secret
realm=coturncoturn7829.666666.xyz

# IPv4-only 监听。公网 IPv6 不稳定时,先不要启用 IPv6。
listening-ip=0.0.0.0
# listening-ip=::
external-ip=你的公网IPv4/10.20.20.8

# 不使用 turns: 时关闭 TLS/DTLS listener,避免 coturn 查找默认证书文件并打印证书警告
no-tls
no-dtls
pidfile=/var/tmp/turnserver.pid

# UDP 中继端口范围
min-port=50000
max-port=50200

# 内网双栈环境里,客户端可能会提交内网 IPv4 或 ULA IPv6 候选地址。
# 如果通话时日志出现大量 403 Forbidden IP,可以先不要启用这些黑名单。
#denied-peer-ip=10.0.0.0-10.255.255.255
#denied-peer-ip=192.168.0.0-192.168.255.255
#denied-peer-ip=172.16.0.0-172.31.255.255

external-ip 很关键:

external-ip=公网IPv4/内网IP

例如 Docker 主机内网 IP 是 10.20.20.8,公网 IPv4 是 153.36.196.208,就写:

external-ip=153.36.196.208/10.20.20.8

创建 /root/data/docker_data/coturn/docker-compose.yaml

services:
  coturn:
    image: coturn/coturn:latest
    container_name: coturn
    restart: unless-stopped
    # 这里不要再写 ports,也不要把 coturn 接入 matrix 网络。
    # network_mode: host 会让 coturn 直接使用宿主机网络栈。
    # 容器内监听的 3478 和 50000-50200/udp 就是宿主机上的端口。
    # 需要在宿主机防火墙/云安全组/路由器里放行:
    # - 3478/tcp
    # - 3478/udp
    # - 50000-50200/udp
    network_mode: host
    command:
      - "-c"
      - "/etc/coturn/turnserver.conf"
      - "--log-file=stdout"
    volumes:
      - /root/data/docker_data/coturn/etc/coturn/turnserver.conf:/etc/coturn/turnserver.conf
      - /root/data/docker_data/coturn/etc/:/etc/

这里显式把 coturn 启动命令固定为读取 /etc/coturn/turnserver.conf,避免镜像默认命令再去自动探测外网 IP。上面的 turnserver.conf 已经设置 no-tlsno-dtls,所以不使用 turns: 时,实际重点是 3478/tcp3478/udp50000-50200/udp

启动:

docker compose up -d
docker logs -f coturn

确认监听:

ss -lunp | grep 3478
ss -ltnp | grep 3478

路由器/防火墙要放行:

3478/tcp
3478/udp
50000-50200/udp

如果 coturn 域名托管在 Cloudflare,可以继续保留在 Cloudflare DNS 里,但小云朵必须关闭,记录类型用 A/AAAA 指向公网 IP。公网 IP 会变化时,用 ddns-go 或 Cloudflare API 自动更新这条 DNS only 记录即可。

十、ntfy 通知服务

ntfy 和 MatrixRTC 没有强依赖,但很多人会顺手把它放在同一台 Docker 主机上,用来接收脚本、监控或系统通知。Tunnel 免 NPM 方案里,ntfy 可以直接使用 host 网络监听宿主机 80,再由 Cloudflare Tunnel 把 https://sms.666666.xyz 转发到 http://10.20.20.8:80

先创建目录:

mkdir -p /root/data/docker_data/ntfy/data/cache
mkdir -p /root/data/docker_data/ntfy/data/config
cd /root/data/docker_data/ntfy

创建 /root/data/docker_data/ntfy/data/config/server.yml

base-url: "https://sms.666666.xyz"
behind-proxy: true
log-level: info

创建 /root/data/docker_data/ntfy/docker-compose.yaml

services:
  ntfy:
    image: binwiederhier/ntfy:latest
    container_name: ntfy
    command:
      - serve
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - /root/data/docker_data/ntfy/data/cache:/var/cache/ntfy
      - /root/data/docker_data/ntfy/data/config:/etc/ntfy
    network_mode: host
    restart: unless-stopped

启动并验证:

docker compose up -d
docker logs -f ntfy
curl -I http://127.0.0.1/
curl -I https://sms.666666.xyz/

注意两点:

  1. 这套写法会让 ntfy 占用宿主机 80/tcp。如果同一台机器上还有 NPM、Caddy 或其他服务占用 80,就不要照搬这一版,改用后面 NPM 文章里的 listen-http: ":2586" 写法。
  2. sms.666666.xyz 是示例域名,实际部署时换成自己的通知域名,不要把真实域名写进公开教程或截图。

十一、启动顺序

第一次部署建议按这个顺序来:

cd /root/data/docker_data/coturn
docker compose up -d

cd /root/data/docker_data/livekit-server
docker compose up -d

cd /root/data/docker_data/matrix-local-federation-proxy
docker compose up -d

cd /root/data/docker_data/matrix-livekit-jwt-service
docker compose up -d

cd /root/data/docker_data/matrix-well-known
docker compose up -d

cd /root/data/docker_data/cloudflared
docker compose up -d

cd /root/data/docker_data/synapse
docker compose up -d

cd /root/data/docker_data/ntfy
docker compose up -d

查看整体状态:

docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

应该能看到:

matrix-synapse
matrix-well-known-web
matrix-livekit-jwt-service
livekit-server
matrix-local-federation-proxy
coturn
cloudflared

十二、完整验证

1. well-known

curl -i https://666666.xyz/.well-known/matrix/client
curl -i https://666666.xyz/.well-known/matrix/server

client 里必须包含:

"livekit_service_url": "https://matrix-jwt.666666.xyz"

2. Synapse

curl -i https://666666.xyz/_matrix/client/versions
curl -i http://127.0.0.1:8448/_matrix/federation/v1/version

3. JWT

curl -i http://127.0.0.1:8170/healthz
curl -i https://matrix-jwt.666666.xyz/healthz

4. LiveKit

curl -I http://127.0.0.1:7880
curl -I https://livekit.666666.xyz

5. 发起呼叫时看日志

开两个 SSH 窗口分别看:

docker logs -f matrix-livekit-jwt-service
docker logs -f livekit-server-test

在 Element 里发起呼叫后,JWT 日志里应出现:

Got Matrix user info for @user:666666.xyz (full access)
Created LiveKit room sid: RM_xxx

LiveKit 日志里应出现:

starting RTC session
participant active
mediaTrack published

如果能看到这些,MatrixRTC / JWT / LiveKit 主链路已经通了。

6. ntfy

curl -I https://sms.666666.xyz/

十三、常见问题

中国大陆网络下 LiveKit 呼叫白屏 8 秒以上

如果现象是:

  • coturn 老通话路径基本正常;
  • 只有 LiveKit / MatrixRTC 呼叫慢;
  • 点击呼叫后 Element 白屏或卡住,过 8 秒甚至更久才弹出呼叫界面;
  • LiveKit 最后能建房,日志里也可能看到 connectionType: udp

那大概率不是 3478 或 coturn relay 的问题,而是 LiveKit 呼叫启动前的控制面链路太慢。一次呼叫至少会涉及:

Element -> matrix-jwt.666666.xyz -> Cloudflare Tunnel -> JWT service
JWT service -> homeserver OpenID 校验
Element -> livekit.666666.xyz -> Cloudflare Tunnel -> LiveKit WebSocket/API
LiveKit 建房、发 token、客户端加入房间

coturn 正常只能说明旧版 TURN 中继链路可用,不能说明 matrix-jwtlivekit 这两个 Tunnel 域名的 HTTPS / WebSocket 延迟足够低。中国大陆网络访问 Cloudflare Tunnel 的稳定性会受到运营商、地区、晚高峰、Cloudflare 边缘节点和 Tunnel 连接质量影响,抖动起来就会表现为呼叫界面迟迟不弹。

这种情况下,不建议继续围绕 coturn 端口排障。更推荐按优先级换方案:

  1. 有公网 80/443:改用公网 443 标准部署,让 NPM 直接处理 Synapse、JWT、LiveKit 的 HTTPS / WSS。
  2. 没有标准 443,但能转发自定义 HTTPS 端口:改用 Cloudflare Worker + NPM,让 Worker 只负责 /.well-known,真实服务走 DNS only + NPM 自定义端口。
  3. 只能使用 Tunnel:保留第六节的本机 federation proxy,尽量减少 JWT 校验绕路;同时接受 LiveKit 呼叫启动可能仍然慢的事实。

判断瓶颈时可以开三个窗口同时看:

docker logs -f cloudflared
docker logs -f matrix-livekit-jwt-service
docker logs -f livekit-server

如果 JWT 日志里 Processing SFU requestGot Matrix user info 很快,但 Element 仍然白屏很久,慢点多半在客户端访问 matrix-jwtlivekit 这两个 Tunnel 公网入口,或者 LiveKit WebSocket 建连阶段。此时继续优化本机 OpenID 校验收益有限,换公网 443 或 Worker + NPM 才是更稳的方向。

发起呼叫白屏几秒

先看 JWT 日志。如果 Processing SFU requestGot Matrix user info 间隔 2-3 秒,通常是 JWT 校验 homeserver 时绕了 Cloudflare Tunnel。

解决办法是启用第六节的 matrix-local-federation-proxy,并给 JWT 服务添加:

extra_hosts:
  - "666666.xyz:127.0.0.1"

以及:

LIVEKIT_INSECURE_SKIP_VERIFY_TLS: "YES_I_KNOW_WHAT_I_AM_DOING"

优化后 Got Matrix user info 通常会和 Processing SFU request 在同一秒出现。剩下 1 秒左右一般是客户端访问 matrix-jwt.666666.xyz 经 Tunnel、LiveKit 建房和 Element 拉起 UI 的时间。

JWT 日志没有任何请求

优先检查:

curl -i https://666666.xyz/.well-known/matrix/client

确认里面有:

"org.matrix.msc4143.rtc_foci"

并且 livekit_service_url 是客户端能访问的公网 HTTPS 地址。

如果客户端是旧版 Element,它可能仍然走 coturn 老通话路径,不会请求 JWT。

JWT 报 OPEN_ID_ERROR

检查三处:

curl -i http://127.0.0.1:8448/_matrix/federation/v1/version
curl -i 'http://127.0.0.1:8448/_matrix/federation/v1/openid/userinfo?access_token=test'
curl -k -i --resolve 666666.xyz:443:127.0.0.1 'https://666666.xyz/_matrix/federation/v1/openid/userinfo?access_token=test'

第一个返回 200,后两个用假 token 返回 401,说明 federation endpoint 和本机代理都是通的。

LiveKit 日志有房间但没声音/没画面

确认路由器/防火墙已经放行 LiveKit UDP 媒体端口:

50201-50400/udp

Cloudflare Tunnel 只转发 7880 的 HTTP/WebSocket,不转发 LiveKit UDP 媒体端口。17882/tcp 只是 TCP fallback;如果没转发但日志里显示 connectionType: udp,说明当前网络已经通过 UDP 正常工作。

coturn 手机端听不到

检查 coturn relay 端口和防火墙端口是否一致。例如配置里是:

min-port=50000
max-port=50200

那路由器/防火墙也必须放行:

50000-50200/udp

同时确认 coturn 域名是 DNS only,不走 Cloudflare 代理。TURN 不能靠 Cloudflare Tunnel 传 UDP 媒体。

Cloudflare Tunnel 访问慢

本文前面的 cloudflared 已经按当前实际配置写成 HTTP/2:

command:
  - tunnel
  - --protocol
  - http2
  - --region
  - us
  - --no-autoupdate
  - run
  - --token
  - "替换成你的 Cloudflare Tunnel token"

如果你之前使用的是 Cloudflare 页面生成的默认命令,可以改成上面这样,然后重建容器:

cd /root/data/docker_data/cloudflared
docker compose up -d

如果当前网络 QUIC 不稳定,HTTP/2 有时更稳。

十四、最终配置清单

这套免 NPM 方案里,每个容器的职责是:

容器 网络 作用
cloudflared host 把 Cloudflare 公网 HTTPS 请求转发到内网端口
matrix-well-known-web bridge/ports 返回 /.well-known/matrix/clientserver
matrix-synapse bridge/ports Matrix homeserver,提供 Client API 和本机 federation 校验
matrix-local-federation-proxy host 只给 JWT 本机校验 Synapse 用,减少 Tunnel 绕路
matrix-livekit-jwt-service host 校验 Matrix 用户并签发 LiveKit token
livekit-server host MatrixRTC 音视频房间和媒体服务
coturn host 旧客户端和 NAT 场景的 TURN 服务
ntfy host 可选通知服务,通过 sms.666666.xyz 走 Tunnel

核心原则就三条:

  1. Cloudflare Tunnel 只处理 HTTP/WebSocket。
  2. LiveKit、JWT、coturn 使用 host 网络,减少 WebRTC 和回环访问问题。
  3. LiveKit UDP、coturn UDP 仍然要在路由器/防火墙真实放行,不能指望 Tunnel 替代。