この記事はまだ日本語に翻訳されていません。原文を表示しています。
他の言語: 繁體中文
公司不想繼續付 Microsoft Teams 或 Webex 的帳號訂閱費?其實只要有一台能跑 Docker 的主機和一個網域,就可以自架一套功能對標 Google Meet 的開源視訊會議系統。這篇文章記錄從零到上線的完整過程,包含踩過的坑和解法。
為什麼選這套
這次用到兩個開源專案的組合:
- La Suite Meet:法國政府(DINUM)官方開源的視訊會議系統,底層用 LiveKit 做 WebRTC,前後端是 React + Django,MIT 授權,法國公務體系正式在用。
- VoidAuth:輕量的自架 SSO / OIDC 身分驗證服務。Meet 本身不管帳號密碼,把登入委派給 OIDC 提供者,VoidAuth 就是扮演這個角色。
成本上,除了電費跟網域費,其他都是免費的。
這篇適合誰
- 有一台能跑 Docker 的主機(實體機或 VPS 都行)
- 有自己的網域,可以設定 DNS
- 對 Linux 指令不陌生,但不需要是專家
架構概覽
這篇用的架構:
- 一台 Docker 主機(
192.168.1.102):跑 Meet、LiveKit、VoidAuth、資料庫等所有容器 - 一台 Caddy 反向代理主機:對外提供 HTTPS,自動申請 Let's Encrypt 憑證
- 三個對外網域(以
sao-x.com為例,換成你自己的)
Caddy 在另一台機器
如果你的 Caddy 跟 Docker 是同一台主機,把所有「另一台主機 IP」改成 127.0.0.1 即可,架構會更簡單。
前置作業:DNS 設定
先把這三個子網域指到你的公網 IP:
meet.sao-x.com A 你的公網IP
auth.sao-x.com A 你的公網IP
livekit.sao-x.com A 你的公網IP不要掛 Cloudflare 代理
這幾個子網域不要開啟 Cloudflare 的橘色雲朵(Proxy)。免費方案會把閒置超過 100 秒的 WebSocket 直接斷掉,導致視訊會議連線不穩定。使用 DNS only(灰色雲朵)即可。
安裝流程
為什麼要分兩階段
有個先天的「雞生蛋」問題:Meet 需要 VoidAuth 給的 OIDC Client ID/Secret,但這組 ID/Secret 必須先把 VoidAuth 跑起來、登入後台手動建立才能拿到。所以拆成兩個階段。
Stage 1:先跑起 VoidAuth
下載安裝腳本後執行:
chmod +x install-meet-voidauth.sh
./install-meet-voidauth.sh stage1腳本會問你幾個問題,有預設值的直接 Enter 就好:
- 根網域(預設
sao-x.com) - Meet / LiveKit / VoidAuth 各自的子網域
- Caddy 主機的內網 IP(用來設定防火牆,只允許這台連進來)
- VoidAuth 資料庫密碼(留空自動產生)
其他密鑰(Storage Key 等)全部自動產生,不需要你記憶。
- 把腳本印出的 Caddy 設定片段貼到你的 Caddyfile:
auth.sao-x.com {
reverse_proxy 192.168.1.102:3000
}- reload Caddy:
systemctl reload caddy - 去
https://auth.sao-x.com登入 VoidAuth 後台(初始密碼在 container log 裡)
在 VoidAuth 建立 OIDC Client
登入後台後,建立一個給 Meet 用的 OIDC Client:
| 欄位 | 值 |
|---|---|
| 名稱 | 任意,例如 meet |
| Redirect URI | https://meet.sao-x.com/api/v1.0/callback/ |
| Grant Type | Authorization Code |
建立後拿到 Client ID 和 Client Secret,記下來備用。
VoidAuth 的 OIDC endpoint 路徑(之後 Stage 2 會用到):
| 功能 | 路徑 |
|---|---|
| 授權 | /oidc/auth |
| Token | /oidc/token |
| UserInfo | /oidc/me |
| JWKS | /oidc/jwks ← 注意不是 /certs |
| Logout | /oidc/session/end |
Stage 2:部署 Meet 全家桶
./install-meet-voidauth.sh stage2這次會問你 OIDC Client ID/Secret(貼上 Stage 1 拿到的)、資料庫密碼、SMTP(沒有就全 Enter 跳過)、管理員帳密。
腳本會自動:啟動所有容器 → 跑 DB migrate → 建立管理員帳號。
完成後把 Meet 和 LiveKit 加到 Caddy:
meet.sao-x.com {
reverse_proxy 192.168.1.102:8083
}
livekit.sao-x.com {
reverse_proxy 192.168.1.102:7880
}最關鍵的最後一步
路由器 Port Forwarding — 最常被漏掉
視訊/音訊的媒體流走 WebRTC,使用者瀏覽器必須能直接連到 LiveKit 容器的 7881/7882,不能走 Caddy 反代。
登入你的路由器後台,新增 Port Forwarding 規則:
- 外部
7881/TCP→192.168.1.102:7881 - 外部
7882/UDP→192.168.1.102:7882
沒做這步的症狀:網頁能開、能登入、能進會議室,但畫面一直轉圈、看不到對方、console 噴 ping timeout。
用 canyouseeme.org 測試 7881 有沒有真的對外開放,確認沒問題就完成了。
踩過的坑(排錯速查)
這是整個過程踩過的坑,整理成一個速查表:
原因:官方範例 env.d/postgresql 裡有一行 POSTGRES_PASSWORD=${DB_PASSWORD},但 docker compose 的 env_file: 是純文字載入,不支援 ${VAR} 展開,所以字面字串 "${DB_PASSWORD}" 被當成密碼傳給 postgres,跟 app 端用的密碼不一樣。
同時要注意 postgres 密碼只有在資料目錄為空時才會套用。
修法:兩個變數寫死同一組真實密碼:
DB_PASSWORD=你的密碼
POSTGRES_PASSWORD=你的密碼如果改過密碼但舊資料目錄還在,砍掉 ./data/databases/backend 重新初始化。
原因:官方範例 env.d/common 帶了 <smtp port>、<smtp host> 這類佔位符,如果沒有設定 SMTP 又沒清掉,Django 啟動時把 <smtp port> 解析成整數直接炸掉。
修法:
sed -i '/<.*>/d' env.d/common
sed -i '/<.*>/d' env.d/postgresql原因:JWKS endpoint 路徑猜錯了,VoidAuth 用的是 /oidc/jwks,不是某些其他 OIDC 實作常見的 /oidc/certs。
修法:
sed -i 's#OIDC_OP_JWKS_ENDPOINT=.*#OIDC_OP_JWKS_ENDPOINT=https://auth.sao-x.com/oidc/jwks#' env.d/common兩個原因都要修:
-
Frontend port 映射錯誤:容器裡 nginx 的真正對外入口是
:8083(docs.conf.template產生的),不是:8080(那只是容器內部靜態伺服器)。docker-compose.override.yaml裡要寫8083:8083,不是8083:8080。 -
兩個必要環境變數沒填:
docs.conf.template用到這兩個變數,沒填的話 nginx 直接拒絕啟動:
BACKEND_INTERNAL_HOST=backend
FRONTEND_INTERNAL_HOST=localhost原因:官方範例 livekit-server.yaml 本身已有一組 keys: 區塊(含佔位符),腳本又附加了一組,YAML 裡出現兩個 keys key,LiveKit 直接拒絕啟動。
修法:手動編輯 livekit-server.yaml,確保整份檔案只有一組 keys: 區塊:
port: 7880
redis:
address: redis:6379
keys:
meetkey: 你的LIVEKIT_API_SECRET
rtc:
udp_port: 7882
tcp_port: 7881
use_external_ip: true原因:路由器沒有做 Port Forwarding,ICE candidate 所有 requestsSent: 8, responsesReceived: 0,媒體封包根本沒收到。
修法:路由器後台新增:
- 外部
7881/TCP→ Docker 主機 IP:7881 - 外部
7882/UDP→ Docker 主機 IP:7882
用 canyouseeme.org 從外部測試 7881 是否真的開放。
開放 API 給 Bot / 第三方整合
Meet 有自己的 Application JWT 系統,讓外部程式能以特定使用者身分呼叫 API,不需要走瀏覽器登入。
啟用 External API
echo "EXTERNAL_API_ENABLED=true" >> env.d/common
echo "APPLICATION_JWT_SECRET_KEY=$(openssl rand -hex 32)" >> env.d/common每行必須獨立
用 echo 一次寫一行,不要把兩個變數寫在同一行。Django 沒辦法解析 TrueAPPLICATION_JWT_SECRET_KEY=xxx 這種粘在一起的值,會直接掛掉。
重啟 backend 後,去 Admin 後台 → Applications → 新增,填入名稱、選擇允許的 scopes 和使用者網域。
用 Application 換 JWT Token
curl -X POST https://meet.sao-x.com/api/v1.0/resource-server/token/ \
-H "Content-Type: application/json" \
-d '{
"client_id": "你的ClientID",
"client_secret": "你的ClientSecret",
"email": "代理的使用者email",
"scope": "rooms:create rooms:list"
}'拿到 token 後,用 Authorization: Bearer <token> header 呼叫 /api/v1.0/resource-server/rooms/ 等端點。
對外 Port 總整理
| Port | 協定 | 在哪台 | 開放對象 | 用途 |
|---|---|---|---|---|
| 80 | TCP | Caddy 主機 | 全網 | HTTP(Let's Encrypt 簽證 + 轉址) |
| 443 | TCP | Caddy 主機 | 全網 | HTTPS(所有網域對外入口) |
| 3000 | TCP | Docker 主機 | 僅 Caddy IP | VoidAuth 反代 |
| 8083 | TCP | Docker 主機 | 僅 Caddy IP | Meet 反代 |
| 7880 | TCP | Docker 主機 | 僅 Caddy IP | LiveKit 信令反代 |
| 7881 | TCP | Docker 主機 | 全網+路由器轉發 | WebRTC 媒體(ICE over TCP) |
| 7882 | UDP | Docker 主機 | 全網+路由器轉發 | WebRTC 媒體(主要影音傳輸) |
真正暴露在公網的只有 Caddy 的 80/443,以及 Docker 主機的 7881/7882(需路由器 Port Forwarding)。其餘 port 用 ufw 限制只對 Caddy 主機的內網 IP 開放。
維護
升級到新版本:
cd ~/meet-stack
# 先看官方 CHANGELOG 有無特殊步驟
docker compose -f docker-compose.meet.yaml -f docker-compose.meet.override.yaml pull
docker compose -f docker-compose.meet.yaml -f docker-compose.meet.override.yaml up -d
docker compose -f docker-compose.meet.yaml -f docker-compose.meet.override.yaml \
run --rm backend python manage.py migrate建議釘版本
範例 compose 檔預設用 latest tag,建議正式上線後把 image 版本號釘死(例如 lasuite/meet-backend:1.17.0),避免自動升級到不相容的新版。
新增使用者:在 VoidAuth 後台的 Invitations 頁面建立邀請連結,發給對方自助註冊。管理員則從 https://meet.sao-x.com/admin 直接操作。