Matrix 管理用 Element Web:Docker Compose 与 NPM 内网 HTTPS 部署

记录在 Alpine Docker 主机上部署 Element Web,并通过 Nginx Proxy Manager 暴露为内网 HTTPS 域名,用来登录 Matrix 管理员账号、获取 Admin API token。

这篇记录一个很实用的小组件:在 Matrix Synapse 同一台 Docker 主机上单独跑一个 Element Web,用来登录管理员账号、获取 access token、验证 Synapse 登录和 MatrixRTC 配置。

它不是 Element Call。这里的 web-call 只是本地目录名,实际跑的是 Element Web 客户端,也就是浏览器版 Matrix 客户端。它适合下面这些场景:

  • 临时登录管理员账号,获取 Synapse Admin API token。
  • 不想在公网 Element、桌面端或手机端里折腾管理员账号。
  • 需要一个固定指向自己 Synapse 的内网 Matrix Web 客户端。
  • 浏览器访问 http://10.20.20.8:8181 提示不支持、白屏或一直转圈。

本文示例用:

Docker 主机内网 IP: 10.20.20.8
Matrix server_name: 666666.xyz
Synapse 客户端地址: https://matrix.666666.xyz:12335
Element Web 域名: https://element.666666.xyz:12335
部署路径: /root/data/docker_data/web-call

实际使用时,把 666666.xyz 换成自己的域名。

一、为什么不直接访问 IP

Element Web 对浏览器安全上下文比较敏感。你直接打开:

http://10.20.20.8:8181

可能会看到:

Element does not support this browser

或者点了继续后一直转圈。这通常不是容器坏了,而是浏览器把普通内网 IP 的 HTTP 页面当成不安全上下文。一些 Web API、存储能力或加密相关能力在这种上下文里会受限制。

SSH 本地转发能用,是因为:

http://localhost:8181

浏览器会把 localhost 当成特殊可信来源。所以临时拿 token 或排障时,可以先用 SSH 本地转发;长期使用再让 NPM 提供 HTTPS 域名访问 Element Web。

如果公网 443 没被封,可以直接用:

https://element.666666.xyz/

如果公网 443 被运营商封了,NPM 可以继续用非标准 HTTPS 端口,例如:

https://element.666666.xyz:12335/

关键点是:浏览器看到的是 HTTPS 域名,而不是明文 IP。

二、准备目录

如果这个目录以前已经存在,想重新部署,可以先删掉再建:

rm -rf /root/data/docker_data/web-call
mkdir -p /root/data/docker_data/web-call
cd /root/data/docker_data/web-call

注意,这只删除 Element Web 客户端目录,不会删除 Synapse 数据库、媒体文件或 Matrix 用户数据。

三、写 Element Web 配置

新建 config.json

vi /root/data/docker_data/web-call/config.json

写入:

{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.666666.xyz:12335",
      "server_name": "666666.xyz"
    }
  },
  "disable_custom_urls": false,
  "disable_guests": true,
  "disable_3pid_login": false,
  "brand": "Element",
  "default_theme": "light",
  "features": {},
  "setting_defaults": {
    "breadcrumbs": true
  },
  "show_labs_settings": true,
  "integrations_ui_url": "",
  "integrations_rest_url": "",
  "integrations_widgets_urls": [],
  "hosting_signup_link": null,
  "map_style_url": ""
}

这里最重要的是:

"base_url": "https://matrix.666666.xyz:12335",
"server_name": "666666.xyz"

base_url 是客户端访问 Synapse 的地址。它必须是浏览器能访问到的 Synapse Client-Server API 地址。

server_name 是 Matrix ID 后面的服务器名。比如你的账号是:

@admin:666666.xyz

那这里就写:

666666.xyz

四、写 docker-compose.yaml

新建:

vi /root/data/docker_data/web-call/docker-compose.yaml

写入:

services:
  web-call:
    image: vectorim/element-web:latest
    container_name: web-call
    restart: unless-stopped
    ports:
      - "8181:80"
    volumes:
      - ./config.json:/app/config.json:ro
    logging:
      driver: "json-file"
      options:
        max-size: "20m"
        max-file: "3"

启动:

cd /root/data/docker_data/web-call
docker compose up -d

检查容器:

docker ps --filter name=web-call

本机验证:

curl -I http://127.0.0.1:8181/
curl -s http://127.0.0.1:8181/config.json | head

如果返回 200 OK,说明 Element Web 容器本身没问题。

五、临时方式:SSH 本地转发访问

如果只是临时登录管理员账号、拿一次 Admin API token,或者还没来得及配置 NPM,可以直接从自己的电脑建立 SSH 本地转发:

ssh -N -L 8181:127.0.0.1:8181 root@10.20.20.8

保持这个 SSH 终端不要关闭,然后在浏览器打开:

http://localhost:8181/

这里的访问路径是:

浏览器 localhost:8181
  -> 本机 SSH 隧道
  -> 10.20.20.8 的 127.0.0.1:8181
  -> web-call 容器

这种方式的优点是简单,不需要 DNS、证书和 NPM;而且 localhost 在浏览器里属于特殊可信来源,通常不会触发直接访问内网 IP 时的安全限制。

如果本机 8181 已经被占用,可以换一个本地端口:

ssh -N -L 18181:127.0.0.1:8181 root@10.20.20.8

浏览器对应打开:

http://localhost:18181/

如果 SSH 连接断开,浏览器页面也会失效。这个方式适合临时使用,不适合作为长期入口。

六、长期方式:NPM 反代配置

进入 Nginx Proxy Manager 的 UI,添加一个 Proxy Host。

Details

Domain Names: element.666666.xyz
Scheme: http
Forward Hostname / IP: 10.20.20.8
Forward Port: 8181
Cache Assets: 关
Block Common Exploits: 开
Websockets Support: 开
Access List: Public

Element Web 是静态前端,反代本身不复杂。Websockets Support 开着即可,虽然 Element Web 访问 Synapse 的请求会直接走 matrix.666666.xyz:12335,不是走这个 element 反代。

SSL

如果你已经有通配符证书,比如:

*.666666.xyz, 666666.xyz

直接选择它。然后打开:

Force SSL: 开
HTTP/2 Support: 开

如果还没有证书,建议用 DNS Challenge 申请通配符证书。公网 80/443 被封时,不要指望普通 HTTP 验证稳定通过。

Advanced

Element Web 通常不需要额外 Advanced 配置,保持空即可。

如果以后想加安全响应头,也要谨慎,尤其不要随意写死过严的 CSP,否则 Element 的登录、加密、跨域请求可能被自己拦掉。

七、NPM 使用非标准 HTTPS 端口

如果公网 443 被封,可以让 NPM 的容器继续把内部 443 映射到宿主机的非标准端口。

例如 /root/data/docker_data/npm/docker-compose.yml

services:
  app:
    image: 'docker.io/jc21/nginx-proxy-manager:latest'
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - '12332:80'
      - '81:81'

      # NPM 内部仍然监听 443,宿主机用 12335 暴露
      - '0.0.0.0:12335:443'
      - '[::]:12335:443'

      # 其他服务也可以继续复用 NPM 443,由 SNI 区分域名
      - '0.0.0.0:12336:443'
      - '[::]:12336:443'
      - '0.0.0.0:12337:443'
      - '[::]:12337:443'
      - '0.0.0.0:12338:443'
      - '[::]:12338:443'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

这种写法的含义是:

浏览器访问 https://element.666666.xyz:12335
        -> 宿主机 12335
        -> NPM 容器 443
        -> 根据 SNI/Host 匹配 element.666666.xyz
        -> 反代到 10.20.20.8:8181

所以即使多个域名都走 :12335,NPM 也能靠域名区分不同 Proxy Host:

https://matrix.666666.xyz:12335
https://element.666666.xyz:12335

只要它们都在 NPM 里有对应 Proxy Host 即可。

改完 NPM compose 后重建:

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

检查端口:

ss -lntp | grep -E ':12335|:8181'

不要再额外映射宿主机标准 443:443,除非你确定公网或内网确实要用标准 443,而且它不会和其他服务冲突。

八、DNS 怎么指

内网想直接打开:

https://element.666666.xyz:12335/

DNS 至少要满足一个条件:

  • 内网 DNS 把 element.666666.xyz 解析到 10.20.20.8
  • 或者公网 DNS 解析到公网 IP,同时路由器支持 NAT Loopback / Hairpin NAT,并把 12335 转发回 10.20.20.8:12335

如果你的电脑解析出来的是公网 IP,但路由器不支持回流,就会出现服务器上测试正常、内网电脑打不开的情况。自用环境最省事的是在内网 DNS 或 DDNS 里直接把 element.666666.xyz 指到 10.20.20.8

可以在客户端电脑上检查:

nslookup element.666666.xyz

期望看到类似:

element.666666.xyz -> 10.20.20.8

九、验证访问

在服务器上可以先强制解析到本机测试:

curl -k -I \
  --resolve element.666666.xyz:12335:127.0.0.1 \
  https://element.666666.xyz:12335/

正常会看到:

HTTP/1.1 200 OK
Server: openresty
X-Served-By: element.666666.xyz

再验证配置文件:

curl -k -s \
  --resolve element.666666.xyz:12335:127.0.0.1 \
  https://element.666666.xyz:12335/config.json

确认里面有:

"base_url": "https://matrix.666666.xyz:12335"

也可以验证 Synapse:

curl -k -I \
  --resolve matrix.666666.xyz:12335:127.0.0.1 \
  https://matrix.666666.xyz:12335/_matrix/client/versions

正常返回:

HTTP/2 200

最后在浏览器打开:

https://element.666666.xyz:12335/

用管理员账号登录即可。

十、获取管理员 access token

登录管理员账号后,可以在浏览器开发者工具里取 token。不同 Element Web 版本界面略有差异,常见方法是:

  1. 打开浏览器开发者工具。
  2. 进入 Application / Storage。
  3. 找到当前站点的 Local Storage 或 IndexedDB。
  4. 搜索 access_token

拿到 token 后,可以测试 Admin API:

curl -s \
  -H "Authorization: Bearer ACCESS_TOKEN_HERE" \
  https://matrix.666666.xyz:12335/_synapse/admin/v1/server_version

如果服务器只在内网访问,也可以在 Synapse 主机上用本地地址测试:

curl -s \
  -H "Authorization: Bearer ACCESS_TOKEN_HERE" \
  http://127.0.0.1:8008/_synapse/admin/v1/server_version

token 很敏感,不要写进公开文章、截图、群聊或 shell 历史。生产环境建议保存到 root 只能读的文件,例如:

mkdir -p /root/.config/synapse-admin
vi /root/.config/synapse-admin/access_token
chmod 600 /root/.config/synapse-admin/access_token

后续脚本里再这样读取:

TOKEN="$(cat /root/.config/synapse-admin/access_token)"

十一、老客户端的会话和密钥问题

如果这个 Element Web 只是临时管理客户端,用完以后不要简单地清浏览器数据就算退出。

Matrix 的端到端加密会把每次登录看成一个设备会话。老版本 Element Web 的“设置 -> 安全与隐私 -> 管理会话”里,有时只能看到其它会话,却无法正常登出其它会话。这样会留下很多旧设备。下次清除浏览器数据、重新登录后,客户端又会把自己当成新设备,并要求重新验证原来的加密密钥或交叉签名。

比较稳妥的做法是:

  1. 临时管理账号尽量只用来拿 Admin API token,不参与加密房间聊天。
  2. 清浏览器数据或重装客户端之前,先在当前仍可用的客户端里确认密钥备份、恢复密钥或安全密钥可用。
  3. 如果老 Element Web 不能删除其它会话,换一个较新的 Element Web、Element Desktop 或 Element X 登录同一账号,在可用的“会话管理”里清理旧设备。
  4. 清理旧设备后,再清除这个临时 Element Web 的浏览器数据。

如果已经清了浏览器数据,又被要求验证旧密钥,就需要从仍然可信的已登录设备验证新会话,或者使用之前保存的恢复密钥/安全密钥恢复。没有可信设备,也没有恢复密钥时,加密房间的旧消息可能无法再解密。

所以这类管理用 Element Web 更适合作为“临时拿 token 的工具”,不要把它当成长期主力加密聊天客户端。

十二、常见问题

打开 IP 提示浏览器不支持

不要用:

http://10.20.20.8:8181

改用:

https://element.666666.xyz:12335/

页面一直转圈

先检查 Element Web 的 config.json

curl -k -s https://element.666666.xyz:12335/config.json

重点看 base_url 是否能从浏览器所在网络访问。如果写成了内网容器名、错误端口或公网不可达地址,就会转圈。

登录时报 homeserver 连接失败

验证:

curl -k -I https://matrix.666666.xyz:12335/_matrix/client/versions

如果这里不通,先修 Synapse 的 NPM 反代,不要先怀疑 Element Web。

NPM UI 看不到反代

不要手写 /data/nginx/proxy_host/*.conf 来长期管理。应该在 NPM UI 里添加 Proxy Host。这样证书、WebSocket、状态、日志和后续修改都能在 UI 里看到。

证书申请失败

公网 80/443 被封时,HTTP-01 验证经常失败。建议使用 Cloudflare DNS Challenge 申请通配符证书,然后所有内网子域名复用这张证书。

十三、最后的推荐形态

最终推荐保持这样:

/root/data/docker_data/web-call/
  docker-compose.yaml
  config.json

Element Web:
  web-call -> 8181:80

NPM Proxy Host:
  element.666666.xyz -> http://10.20.20.8:8181

访问地址:
  https://element.666666.xyz:12335/

Synapse:
  https://matrix.666666.xyz:12335

这套方式的好处是:Element Web 容器简单,NPM UI 可见,证书统一管理,不依赖 SSH 隧道,也不要求运营商放行公网标准 443