从零部署 LiveKit:搭建私有实时音视频通话服务器

📑 目录

  1. 快速部署 --- 先跑起来
    • [1.1 Docker 环境](#1.1 Docker 环境)
    • [1.2 ★ LiveKit 安装(3 步)](#1.2 ★ LiveKit 安装(3 步))
    • [1.3 内网测试(无域名)](#1.3 内网测试(无域名))
    • [1.4 配套组件](#1.4 配套组件)
    • [1.5 AppServer](#1.5 AppServer) --- SDK 角色 + 核心代码
  2. 验证部署
  3. [LiveKit 配置详解](#LiveKit 配置详解) --- 每项原理
  4. 系统架构 --- 理解全貌
  5. 环境要求
  6. 端口与网络
  7. 数据库初始化
  8. 运维监控
  9. 故障排查
  10. 性能优化
  11. 安全加固
  12. [常见问题 FAQ](#常见问题 FAQ)
  13. 附录

一、快速部署

本章直接给你可执行的命令。跑通之后,后面章节逐层讲原理:[LiveKit 配置详解](#LiveKit 配置详解) → 系统架构端口规划故障排查

1.1 前置准备:Docker 环境

安装 Docker
bash 复制代码
# CentOS
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
systemctl enable docker --now

# Ubuntu
apt update && apt install -y docker.io docker-compose-v2
systemctl enable docker --now

# 验证
docker --version          # ≥ 24.0
docker compose version    # ≥ v2.20
配置镜像加速(国内拉取更快)

Docker 默认从 docker.io(Docker Hub)拉取镜像,国内直连速度慢。通过 registry-mirrors 让 Docker 先走国内镜像源:

bash 复制代码
cat > /etc/docker/daemon.json << 'EOF'
{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://hub.rat.dev"
  ]
}
EOF

systemctl restart docker

docker.1ms.runhub.rat.dev 是社区提供的 Docker Hub 镜像。你也可以换成其他镜像源,比如 https://mirror.ccs.tencentyun.com

配置后,docker pull livekit/livekit-server:latest 实际拉取链路:

复制代码
docker pull livekit/livekit-server:latest
    │
    ▼
Docker Daemon 检查 registry-mirrors
    │
    ├─① 先试: docker.1ms.run/livekit/livekit-server:latest
    ├─② 再试: hub.rat.dev/livekit/livekit-server:latest
    └─③ 兜底: docker.io/livekit/livekit-server:latest (Docker Hub 直连)

1.2 ★ LiveKit 安装(2 步搞定 + 验证)

复制代码
┌──────────────┐    ┌──────────────────┐    ┌──────────────────┐
│ 第 1 步       │──►│ 第 2 步            │──►│ 验证              │
│ 自动生成全部  │    │ docker compose    │    │ curl :7880       │
│ 配置文件+密钥 │    │ up -d 启动        │    │ 看到 {"ok"}      │
└──────────────┘    └──────────────────┘    └──────────────────┘
bash 复制代码
# ═══════════════════════════════════════════
# 第 1 步:用 livekit/generate 自动生成全部配置文件
# ═══════════════════════════════════════════
# generate 会根据你输入的域名创建同名子目录
# 所以先进入部署的父目录,不要在提前创建子目录
mkdir -p /opt/livekit
cd /opt/livekit

# 拉取配置生成器
docker pull livekit/generate

# 拉取 LiveKit Server 镜像(后续 docker compose pull 也会自动拉取)
docker pull livekit/livekit-server:latest

![docker pull 效果](images/docker-pull-result.png)

# 运行生成器(交互式)
docker run --rm -it -v $PWD:/output livekit/generate

完整交互过程(上下箭头选择,回车确认):

复制代码
Generating config for production LiveKit deployment
This deployment will utilize docker-compose and Caddy.
It'll set up a secure LiveKit installation with built-in TURN/TLS
SSL Certificates for HTTPS and TURN/TLS will be generated automatically
via LetsEncrypt or ZeroSSL.

? What to deploy:
    LiveKit Server only
  ▸ with Egress                          ← 选这个(需要录音)
    with Ingress
    with both Egress and Ingress

? Primary domain name:
  livekit.your-domain.com                  ← 你的主域名

? TURN domain name:
  livekit-turn.your-domain.com             ← 主域名前面加 -turn

  (TURN 域名也要配 DNS A 记录,指向同一台服务器)

? Which SSL issuers to use?:
  ▸ Let's Encrypt (no account required)  ← 免费,无需注册
    ZeroSSL (best compatibility, requires account)

? LiveKit version:
    custom
  ▸ latest                               ← 最新稳定版
    v1.13.2

? Use external Redis:
  ▸ no - (we'll bundle Redis)            ← 自带 Redis,省事
    yes

? Generate a startup script?:
  ▸ Startup Shell Script                 ← 生成 init_*.sh
    Cloud Init for Amazon Linux
    Cloud Init for Ubuntu
    Skip

? Email:
  your@email.com                         ← 证书过期提醒用

生成完毕后输出你的专属密钥(记下来,后续 AppServer 配置要用):

复制代码
Server URL: wss://livekit.your-domain.com
API Key: APIxxxxxxxxxxxx
API Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Here's a test token generated with your keys: eyJhbGciOi...

同时所有配置文件已自动生成到 livekit.your-domain.com/ 目录下,密钥已自动填入,无需手动改任何东西。

生成后的目录(由 generate 自动创建,名字 = 你的域名):

复制代码
/opt/livekit/
└── livekit.your-domain.com/      ← 目录名就是你的域名
    ├── livekit.yaml            # ★ 密钥、端口、TURN、Webhook 全部配好
    ├── docker-compose.yaml     # 4 个服务:caddy + livekit + redis + egress
    ├── caddy.yaml              # 自动配好 TLS 证书 + 域名
    ├── egress.yaml             # 录音配置
    ├── redis.conf              # Redis 持久化
    └── init_*.sh               # 初始化脚本(可选)

这就是为什么目录叫 livekit.your-domain.com ------ LiveKit 以域名作为部署单元,一个域名一套配置。你上来做的就是有域名的部署,所以直接用 livekit/generate(不需要 --local)。

如果是内网测试(无域名,纯 IP):

bash 复制代码
docker run --rm -v $PWD:/output livekit/generate --local
# 不问你域名,直接生成内网配置
bash 复制代码
# ═══════════════════════════════════════════
# 第 2 步:拉取镜像 + 启动
# ═══════════════════════════════════════════
# generate 已经在 /opt/livekit 下创建了以域名命名的子目录
cd /opt/livekit/livekit.your-domain.com

# 拉取 docker-compose.yaml 中定义的全部镜像
# 包含: livekit/livekit-server + livekit/caddyl4 + livekit/egress + redis:7-alpine
docker compose pull

# 启动所有服务
docker compose up -d

# 验证
curl -s http://127.0.0.1:7880
# → {"message":"ok"}

# 查看运行状态
docker compose ps

docker compose pull 一步拉取全部 4 个镜像,不需要单独 docker pull livekit/livekit-server,因为 livekit/generate 生成的 docker-compose.yaml 已经定义好了这些镜像,compose pull 会一并处理。

至此 LiveKit 已运行。 配置文件、密钥、服务编排全部由 livekit/generate 1 自动完成。接下来 验证部署,然后深入 配置原理


1.3 内网测试环境(无域名、无 SSL)

livekit/generate --local 即可,一行生成内网配置:

bash 复制代码
mkdir -p /opt/livekit/livekit-intranet && cd /opt/livekit/livekit-intranet
docker run --rm -v $PWD:/output livekit/generate --local
docker compose pull
docker compose up -d

# 连接地址: ws://服务器IP:7880

1.4 看一眼生成的配置文件

livekit/generate 自动产出了 5 个文件,无需手写:

bash 复制代码
cd /opt/livekit/livekit.your-domain.com
ls -la
# livekit.yaml           ← LiveKit 核心配置(密钥已自动填入)
# docker-compose.yaml    ← 4 个服务编排(caddy + livekit + redis + egress)
# caddy.yaml             ← Caddy L4 代理 + TLS
# egress.yaml            ← 录音服务
# redis.conf             ← Redis 持久化
常用运维命令
bash 复制代码
cd /opt/livekit/livekit.your-domain.com

docker compose ps                          # 查看状态
docker compose restart livekit             # 重启 LiveKit
docker compose logs -f livekit             # 实时日志
docker compose up -d --force-recreate livekit  # 重建容器
docker compose pull livekit && docker compose up -d livekit  # 升级
docker exec -it $(docker compose ps -q livekit) /bin/sh        # 进容器调试

1.5 AppServer 业务后端

AppServer 是 LiveKit 与客户端之间的业务调度层。要理解它的定位,需要先看清两个 SDK 的角色(参考 LiveKit 官方 SDK 生态 2):

复制代码
┌──────────────────────────────────────────────────────────────┐
│                        你的系统                               │
│                                                              │
│  Flutter App                     Python AppServer            │
│  ┌─────────────┐                 ┌──────────────┐            │
│  │ Client SDK  │  ←── WS ───→   │ Server SDK   │            │
│  │ (前端媒体)   │   信令+Token    │ (后端调度)    │            │
│  └──────┬──────┘                 └──────┬───────┘            │
│         │                               │                    │
│         │  WebRTC 媒体                   │  API / Webhook     │
│         │                               │                    │
│         └─────── LiveKit Server ────────┘                    │
│                   (SFU 中转)                                  │
└──────────────────────────────────────────────────────────────┘
SDK 类型 跑在哪里 干什么 本项目用的
Client SDK Flutter App 内 WebRTC 采集麦克风、播放音频、房间连接 livekit_client (Flutter)
Server SDK AppServer 内 签发 JWT Token、调 LiveKit API、收 Webhook livekit-api (Python)

Client SDK 只管媒体 (音视频的采集与播放),Server SDK 只管调度 (谁可以进哪个房间、何时启动录音)。两者通过 LiveKit Server 对接。AppServer 就是 Server SDK 的载体,承担 Token 签发、WebSocket 信令、Webhook 录音触发。

AppServer 核心模块

三个关键模块串联起完整通话流程:

模块一:WebSocket 连接管理 --- 在线状态 + 呼叫信令

python 复制代码
class ConnectionManager:
    def __init__(self):
        self.active_connections: dict[str, WebSocket] = {}   # 谁在线
        self.names: dict[str, str] = {}                     # 显示名称
        self.pending_calls: dict[str, dict] = {}            # 进行中的呼叫
        self.missed_calls: dict[str, list[dict]] = {}       # 离线未接来电积压

客户端连上 → 记入 active_connections → 广播在线列表 → 发起/接听/挂断全部走 WS。

模块二:Token 签发 --- 客户端凭此进入 LiveKit 房间

python 复制代码
@app.post("/api/token")
async def generate_token(identity: str, room_name: str):
    token = lk_api.AccessToken(api_key=..., api_secret=...) \
        .with_identity(identity) \
        .with_grants(lk_api.VideoGrants(
            room_join=True, room=room_name,
            can_publish=True, can_subscribe=True,
        )) \
        .to_jwt()
    # 返回给客户端 → 客户端用 token 连接 LiveKit 房间

模块三:Webhook --- 录音自动触发

python 复制代码
@app.post("/api/webhook/livekit")
async def livekit_webhook(request: Request):
    event = lk_api.WebhookReceiver(...).receive(body, auth_header)
    if event.event == "participant_joined":
        await _auto_start_recording_on_join(room_name)   # 第二人进房 → 自动录音
    elif event.event == "egress_ended":
        ...  # 录音结束 → 更新 recording_files 表

三个模块的协作关系:

复制代码
WS连接 ──→ ConnectionManager(在线)
  │
  ├─ 呼叫 ──→ pending_calls(信令) ──→ 对方收到 incoming_call
  │
  └─ 接听 ──→ /api/token(模块二) ──→ 双方各拿 Token
                  │
                  └─→ 连 LiveKit 房间 ──→ participant_joined Webhook
                                               │
                                               └─→ 模块三触发 Egress 录音
环境配置
bash 复制代码
mkdir -p /opt/livekit/AppServer/{logs,recordings}
cd /opt/livekit/AppServer
bash 复制代码
# .env
cat > .env << 'EOF'
LIVEKIT_URL=http://127.0.0.1:7880
LIVEKIT_API_KEY=你的API_KEY
LIVEKIT_API_SECRET=你的API_SECRET
DATABASE_URL=mysql+asyncmy://user:password@your-db-host:3306/your-db-name
APP_PORT=8080
LOG_LEVEL=INFO
LOG_DIR=/opt/livekit/AppServer/logs
LOG_RETENTION_DAYS=3
EOF
安装与部署
bash 复制代码
# conda 环境
conda create -n appsrv python=3.11 -y && conda activate appsrv

# 安装依赖(Server SDK: livekit-api 实现 Token + Webhook)
pip install fastapi uvicorn livekit-api pyjwt sqlalchemy asyncmy minio python-dotenv aiohttp protobuf websockets

# 核心文件 4 个
# main.py          主程序:WS 信令 + Token + 录音 + Webhook + 监控
# models.py        数据模型:call_records + recording_files
# config.py        环境变量读取
# system_monitor.py 系统资源监控

# 从本地上传
scp AppServer/*.py root@your-server-ip:/opt/livekit/AppServer/

# 启动
uvicorn main:app --host 0.0.0.0 --port 8080
systemd 守护
bash 复制代码
cat > /etc/systemd/system/appsrv.service << 'EOF'
[Unit]
Description=LiveKit AppServer
After=network.target docker.service

[Service]
Type=simple
User=root
WorkingDirectory=/opt/livekit/AppServer
ExecStart=/opt/livekit/anaconda3/envs/appsrv/bin/uvicorn main:app --host 0.0.0.0 --port 8080
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable appsrv --now
systemctl status appsrv

二、验证部署是否成功

部署完成(一、快速部署)后,按本章确认一切正常。有问题参考 九、故障排查

2.1 启动顺序

复制代码
MySQL(远程) → Redis → LiveKit → Caddy → AppServer
                                  → Egress
bash 复制代码
# 从头启动
cd /opt/livekit/livekit.your-domain.com
docker compose up -d redis       # 先 Redis
sleep 3
docker compose up -d livekit     # 再 LiveKit
sleep 2
docker compose up -d caddy egress # 最后代理和录音
systemctl start appsrv            # AppServer

2.2 健康检查

bash 复制代码
# AppServer
curl http://localhost:8080/api/health
# → {"status":"ok"}

# 详细状态
curl http://localhost:8080/api/health/detail
# → {"status":"ok","db_ok":true,"online_users":0,"active_calls":0,...}

# LiveKit
curl -s http://127.0.0.1:7880
# → {"message":"ok"}

# Caddy → LiveKit
curl -s -k https://localhost/
# → {"message":"ok"}

# Redis
docker exec $(docker ps -q -f name=redis) redis-cli ping
# → PONG

# WebSocket
wscat -c wss://livekit.your-domain.com
# 连接后输入 {"type":"ping"} → 收到 {"type":"pong"}

2.3 端到端测试

浏览器打开 livekit-test-production.html,两标签页加入同一房间测试音频互通。


三、LiveKit 配置详解

上面的部署让你跑起来了。下面解释每个配置项的原理。

3.1 livekit.yaml 完整参考

yaml 复制代码
# ── 基础 ──
port: 7880                              # API / WebSocket 监听端口
bind_addresses: [""]                    # "" 表示监听所有网卡

# ── WebRTC ──
rtc:
    tcp_port: 7881                      # TCP fallback(UDP 不通时走 TCP)
    port_range_start: 50000             # UDP 媒体端口起始
    port_range_end: 60000               # UDP 媒体端口结束
    use_external_ip: true               # 自动探测公网 IP
    enable_loopback_candidate: false    # 禁用 127.0.0.1

# ── Redis ──
redis:
    address: localhost:6379
    username: ""
    password: ""
    db: 0
    use_tls: false

# ── TURN ──
turn:
    enabled: true
    domain: livekit-turn.your-domain.com
    tls_port: 5349
    udp_port: 3478
    external_tls: true

# ── API 认证 ──
keys:
    YOUR_KEY: YOUR_SECRET

# ── Webhook ──
webhook:
    urls:
        - http://localhost:8080/api/webhook/livekit
    api_key: YOUR_KEY

3.2 每个配置项的作用

配置项 干什么 配错/不配的后果
port: 7880 LiveKit 内部 API + WebSocket 端口 AppServer 无法签发 Token
rtc.port_range WebRTC 媒体 UDP 端口池 听不到声音,没有媒体数据
rtc.use_external_ip 自动获取公网 IP 填 ICE candidate 客户端 P2P 连接失败,强制走 TURN 中继
redis 房间状态缓存,Egress 任务队列 房间无法创建,录音无法启动
turn NAT 穿透兜底(对称型 NAT 用户) 部分用户无法通话
keys API 调用鉴权 AppServer 签发 Token 时报 401
webhook 房间事件回调(进房/离房/销毁) 录音无法自动启停

3.3 RTC 端口池规模计算

端口范围 50000 , 60000 50000, 60000 50000,60000,总计 10001 10001 10001 个端口。

每个 WebRTC PeerConnection 消耗约 2 ∼ 4 2 \sim 4 2∼4 个端口:

C m a x = 10001 4 ≈ 2500 路并发 C_{max} = \frac{10001}{4} \approx 2500 \text{ 路并发} Cmax=410001≈2500 路并发

实际瓶颈在 CPU 和带宽,单机建议 200~500 路。

3.4 Token TTL 设计

Token 有效期由 AppServer 的 TOKEN_TTL_HOURS 环境变量控制,默认 24 小时:

T v a l i d = T T L h o u r s × 3600 秒 T_{valid} = TTL_{hours} \times 3600 \text{ 秒} Tvalid=TTLhours×3600 秒

python 复制代码
# AppServer/main.py 中的 Token 签发逻辑
token = lk_api.AccessToken(api_key, api_secret) \
    .with_identity(identity) \
    .with_name(display_name) \
    .with_grants(lk_api.VideoGrants(
        room_join=True, room=room_name,
        can_publish=True, can_subscribe=True,
    )) \
    .with_ttl(datetime.timedelta(hours=TOKEN_TTL_HOURS)) \
    .to_jwt()

3.5 数据流:从呼叫到通话音頻

复制代码
Flutter A (主叫)     AppServer(:8080)      Flutter B (被叫)      LiveKit(:7880)
    │                     │                     │                    │
    │─① WS call(B)──────►│                     │                    │
    │                     │─② WS incoming─────►│                    │
    │◄─③ WS ringing──────│                     │                    │
    │                     │◄─④ WS accept───────│                    │
    │                     │─⑤ 签发 A+B Token───│                    │
    │◄─⑥ WS accepted─────│─⑥ WS accepted─────►│                    │
    │                     │                     │                    │
    │──⑦ connect(token)─────────────────────────────────────────►│
    │                     │                     │──⑦ connect(token)►│
    │◄═══════════════ ⑧ WebRTC 音频 (SFU 中转) ══════════════════►│

信令走 WebSocket,媒体走 WebRTC。控制面与数据面分离


四、系统架构

前面 快速部署 让你跑起来了,配置详解 解释了每个参数。现在从整体视角理解系统是如何协同工作的 26

4.1 架构全景图

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                         Internet                                 │
│  Flutter App ◄──── wss://livekit.your-domain.com:443 ────►        │
└───────────────────────────┬──────────────────────────────────────┘
                            │
                   ┌────────▼────────┐
                   │  Caddy (L4 代理) │  TLS 终结 + SNI 路由
                   │  :443 (TLS)     │
                   │  :5349 (TURNS)  │
                   └──┬──────────┬───┘
                      │          │
         ┌────────────▼──┐  ┌───▼───────────────┐
         │ ★ LiveKit SFU │  │ TURN Server       │
         │ :7880 (API)   │  │ :3478 (STUN/UDP)  │
         │ :7881 (TCP)   │  │ :5349 (TURNS/TLS) │
         │ :50000-60000  │  └───────────────────┘
         │   (UDP 媒体)   │
         └───────┬────────┘
                 │
    ┌────────────┼────────────────┐
    │            │                │
┌───▼──┐  ┌─────▼──────┐  ┌─────▼────────┐
│Redis │  │Egress(录音) │  │AppServer:8080│
│:6379 │  │:内部        │  │FastAPI + WS  │
└──────┘  └──────┬──────┘  └──┬───────────┘
                 │             │
           ┌─────▼─────┐  ┌───▼────┐
           │ 录音文件    │  │ MySQL  │
           │ /recordings│  │ 远程DB  │
           └───────────┘  └────────┘

4.2 组件职责一览

组件 一句话职责 端口 挂掉的影响
LiveKit WebRTC 媒体 SFU 中转 7880/7881/UDP池 整个通话不可用
Caddy TLS 终结 + L4 反向代理 443 公网客户端无法连接
AppServer Token 签发 + 信令 + 录音触发 8080 无法发起/接听通话, 录音停摆
Egress 服务端录音 MP3 编码 内部 通话正常但无录音
Redis 房间状态 + Egress 任务队列 6379 无法创建房间, 录音失败
TURN NAT 穿透兜底 3478/5349 对称型NAT用户无法通话
MySQL 通话记录持久化 3306(远程) 记录不落库, 无法查历史

4.3 Webhook 录音链路

复制代码
房间参与者变动 → LiveKit POST webhook → AppServer
    → handle_participant_joined()
    → 判断 ≥2 个真实参与者
    → LiveKit API: start_egress(room_name)
    → Redis 通知 Egress
    → Egress 入房 → 接收音频 → MP3 编码 → 写入磁盘

停止录音四重保障(按优先级):

  1. handle_hangup --- 挂断信令 → 主动停(最可靠)
  2. participant_left --- 房间无真实用户 → 自动停
  3. room_finished --- 房间销毁 → 清理 RecordingFile
  4. 客户端 HTTP → REST API 手动停(兜底)

五、环境要求

资源 最低 推荐 备注
CPU 4 核 8 核+ Egress 1 核/路录音
内存 8 GB 16 GB+ AppServer ~500MB, Redis ~100MB
磁盘 50 GB 200 GB+ 录音 1MB/分钟
带宽 10 Mbps 50 Mbps+ ~100Kbps/路
Docker ≥ 24.0 最新 ---
Python ≥ 3.10 3.11 AppServer

Python 依赖:

txt 复制代码
fastapi>=0.115.0        # Web 框架
uvicorn>=0.32.0         # ASGI 服务器
livekit-api>=1.0.0      # LiveKit 服务端 SDK
pyjwt>=2.9.0            # JWT
sqlalchemy>=2.0.0       # ORM
asyncmy>=0.2.0          # MySQL 异步驱动
minio>=7.2.0            # 对象存储
python-dotenv>=1.0.0    # .env
aiohttp>=3.10.0         # 异步 HTTP
protobuf>=5.0.0         # LiveKit Protocol

六、端口规划与防火墙

6.1 端口映射

端口 协议 用途 公网
443 TCP Caddy WSS/HTTPS (TLS)
443 UDP TURN/TLS 备选
5349 TCP/UDP TURN TLS
3478 UDP TURN STUN
8080 TCP AppServer REST + WS
50000-60000 UDP WebRTC 媒体端口池
7880 TCP LiveKit 内部 API
7881 TCP LiveKit RTC TCP
6379 TCP Redis
9000/9001 TCP MinIO

6.2 443 端口被占用,怎么换?

如果 443 端口已被其他程序(IIS、nginx、Skype 等)占用,换成 8443

bash 复制代码
cd ~/LMZPro/LiveKit/livekit.your-domain.com

# 一行命令换端口
sed -i 's/listen: \[":443"\]/listen: [":8443"]/' caddy.yaml

# 重启 Caddy
docker compose restart caddy

改后 Caddy 监听 :8443,需要同步更新三处:

位置 改什么
caddy.yaml listen: [":8443"] ✅ 上面 sed 已改
阿里云安全组 放行 TCP 8443
AppServer .env LIVEKIT_URL=https://livekit.your-domain.com:8443
客户端 ws_url 换成 wss://livekit.your-domain.com:8443

6.3 防火墙配置

bash 复制代码
# 公网端口
iptables -A INPUT -i eth0 -p tcp --dport 8080 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --dport 5349 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 3478 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 50000:60000 -j ACCEPT

# 内网端口拒绝外网访问
iptables -A INPUT -i eth0 -p tcp --dport 7880 -j DROP
iptables -A INPUT -i eth0 -p tcp --dport 6379 -j DROP

七、数据库初始化

7.1 MySQL 连接

项目
主机 your-db-host:3306
数据库 your-db-name
用户 your-db-name
密码 your-db-password

7.2 建表 SQL

sql 复制代码
CREATE TABLE IF NOT EXISTS call_records (
    id INT AUTO_INCREMENT PRIMARY KEY,
    room_name VARCHAR(255) NOT NULL COMMENT 'LiveKit房间名',
    caller_identity VARCHAR(255) NOT NULL COMMENT '主叫ID',
    caller_name VARCHAR(255) COMMENT '主叫名称',
    callee_identity VARCHAR(255) COMMENT '被叫ID',
    callee_name VARCHAR(255) COMMENT '被叫名称',
    status VARCHAR(32) NOT NULL DEFAULT 'pending'
        COMMENT 'pending/active/ended/failed',
    started_at DATETIME COMMENT '开始时间',
    ended_at DATETIME COMMENT '结束时间',
    duration_seconds INT COMMENT '通话时长(秒)',
    INDEX idx_room (room_name),
    INDEX idx_caller (caller_identity),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS recording_files (
    id INT AUTO_INCREMENT PRIMARY KEY,
    call_record_id INT NOT NULL COMMENT '关联call_records.id',
    egress_id VARCHAR(255) NOT NULL UNIQUE COMMENT 'Egress任务ID',
    file_path TEXT COMMENT '文件路径',
    file_size_bytes INT COMMENT '文件大小',
    status VARCHAR(32) NOT NULL DEFAULT 'started'
        COMMENT 'started/ended/failed',
    started_at DATETIME,
    ended_at DATETIME,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_call_record (call_record_id),
    FOREIGN KEY (call_record_id) REFERENCES call_records(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

7.3 ER 图

复制代码
┌──────────────────┐          ┌──────────────────┐
│   call_records   │          │ recording_files  │
├──────────────────┤          ├──────────────────┤
│ id (PK)          │◄────────┤ call_record_id(FK)│
│ room_name        │   1:N    │ id (PK)          │
│ caller_identity  │          │ egress_id (UK)   │
│ callee_identity  │          │ file_path        │
│ status           │          │ status           │
│ started_at       │          │ started_at       │
│ ended_at         │          │ ended_at         │
│ duration_seconds │          └──────────────────┘
└──────────────────┘

八、运维监控体系

8.1 核心指标

bash 复制代码
curl http://localhost:8080/api/health/detail
指标 告警阈值 动作
concurrent_recordings / limit > 80% Egress CPU 即将饱和
recordings_last_1h_failed > 0 录音链路故障
active_calls / limit > 80% 即将拒绝新呼叫
db_ok false 数据库不可用
online_users / limit > 90% 即将拒绝新连接

8.2 系统资源监控

AppServer 内置 _system_monitor_task,每秒采集:

  • CPU > 85% 或 MEM > 90% 或 DISK > 90% → 拒绝新呼叫 ,返回 call_failed: "服务器繁忙"
  • 监控数据异常时降级放行,优先保证可用性

可通过环境变量调阈值:

参数 默认
MONITOR_CPU_THRESHOLD 85%
MONITOR_MEM_THRESHOLD 90%
MONITOR_DISK_THRESHOLD 90%

8.3 日志管理

bash 复制代码
# AppServer(按天轮转,保留 3 天)
tail -f /opt/livekit/AppServer/logs/appserver.log

# systemd
journalctl -u appsrv -f

# 关键词过滤
grep "ERROR\|WARNING" /opt/livekit/AppServer/logs/appserver.log
grep "上线\|下线\|呼叫\|挂断\|录音" /opt/livekit/AppServer/logs/appserver.log

# Docker 日志
docker compose -f /opt/livekit/livekit.your-domain.com/docker-compose.yaml logs livekit --tail 50
docker compose -f /opt/livekit/livekit.your-domain.com/docker-compose.yaml logs egress --tail 30

8.4 资源消耗参考

组件 消耗 瓶颈 单路参考
LiveKit 网络带宽 带宽 ~100 Kbps
Egress CPU CPU 核数 1 核/路
AppServer 内存 内存 ~50 KB/连接
Redis 内存 内存 < 100 MB
Caddy CPU (TLS) CPU 极低

Egress 是唯一瓶颈。并发录音数超过 CPU 核数则新录音启动失败。


九、故障排查指南

遇到问题先查下表,然后按 [9.2 专项排查](#9.2 专项排查) 逐项定位。涉及配置修改后参考 三、配置详解 确认参数正确性。

9.1 速查表

现象 原因 排查 解决
Flutter 连不上 端口不可达/安全组 curl IP:8090/api/health 放行安全组
WebSocket 404 uvicorn 缺 websockets 库 `journalctl -u appsrv grep WebSocket`
录音无文件 目录挂载/权限/路径 docker exec egress find /recordings 补挂载卷 + chmod 777
录音未自动启动 Webhook / LIVEKIT_URL grep "录音自动启动" logs/ 确认 .env URL 正确
Egress 反复失败 ws_url 指向错误地址 grep ws_url egress.yaml 改为 ws://localhost:7880
Egress x509 证书错误 Caddy 移除后仍连 wss docker logs egress --tail 30 ws_url 用 ws 不用 wss
CPU 过载拒呼 并发录音过多 curl /api/health/detail 降低并发/扩容
听不到声音 UDP 端口被封 nc -u IP 50000-60000 查防火墙
通话记录不落库 MySQL 不可达 curl /api/health/detail 查 DB 连接

9.2 录音故障专项排查(迁移常见踩坑)

以下场景特指 Caddy 移除后 LiveKit 直连 时的录音问题:

问题一:录音启动成功但文件不生成

复制代码
┌─ 排查链路 ───────────────────────────────────────┐
│ 1. docker exec livekit-your-domain-egress-1 find /recordings -name "*.mp3" │
│    → 有文件 → 挂载路径不对,文件在容器内但没映射到宿主机                    │
│    → 无文件 → 进第 2 步                                                   │
│ 2. docker logs livekit-your-domain-egress-1 --tail 30                     │
│    → "x509: certificate expired" → ws_url 用了 wss,需改成 ws              │
│    → "could not dial signal connection" → egress.yaml 指向了外网域名       │
│ 3. grep recordings docker-compose.yaml                                     │
│    → 无结果 → volumes 挂载缺失,需补:                                       │
│      volumes:                                                               │
│        - ./egress.yaml:/etc/egress.yaml                                     │
│        - /opt/livekit/AppServer/recordings:/recordings        │
└──────────────────────────────────────────────────┘

问题二:Egress ws_url 导致证书/连接失败

bash 复制代码
# 原因:egress.yaml 中 ws_url 指向 wss://域名(走 Caddy),
# Caddy 移除后证书失效,Egress 无法连上 LiveKit

# 修复:改为内网直连
sed -i 's|ws_url:.*|ws_url: ws://localhost:7880|' egress.yaml
docker compose restart egress

问题三:AppServer 连不上 LiveKit 导致录音 API 500

bash 复制代码
# 原因:.env 中 LIVEKIT_URL 仍指向 https://域名:8443(走 Caddy)
# Caddy 移除后此地址不可达

# 修复:改为内网直连
# .env 中: LIVEKIT_URL=http://127.0.0.1:7880
systemctl restart appsrv

修复后验证:

bash 复制代码
# 1. 确认 Egress 无报错
docker logs livekit-your-domain-egress-1 --tail 10 | grep -E "ERROR|error|failed"

# 2. 确认录音目录可写
ls -la /opt/livekit/AppServer/recordings/

# 3. 打一通测试电话后确认文件生成
find /opt/livekit/AppServer/recordings/ -name "*.mp3"

WebSocket 频繁断开:

复制代码
journalctl -u appsrv --since "5 min ago" | grep "断开"
  → 客户端主动断 or 网络超时
  → 同一秒多次重连 → 客户端需加退避

十、性能优化建议

10.1 LiveKit

yaml 复制代码
# 生产调优
rtc:
    use_external_ip: true
    enable_loopback_candidate: false
    # 纯语音场景可关闭 TCP fallback 以减少连接开销
    # tcp_port: 7881

10.2 AppServer

bash 复制代码
uvicorn main:app --host 0.0.0.0 --port 8080 \
    --limit-concurrency 1000 \
    --backlog 2048 \
    --timeout-keep-alive 65

WebSocket 状态是内存维护的,不要用 --workers N 多进程,否则跨 worker 在线状态不一致。

10.3 容量规划

若 CPU 为 8 核:

R m a x = min ⁡ ( 8 − 2 , 10 ) = 6 路并发录音 R_{max} = \min(8 - 2, \ 10) = 6 \text{ 路并发录音} Rmax=min(8−2, 10)=6 路并发录音

留 2 核给系统。Egress 1 路 = 1 核。


十一、安全加固清单

P0 必做:

  • 替换 livekit.yaml 中默认 Key/Secret → docker run --rm livekit/livekit-server generate-keys
  • 修改数据库密码
  • 防火墙仅开放必需端口(443/5349/3478/8080/50000-60000)
  • SSH 密钥认证 + 改端口

P1 建议:

  • Webhook 签名验证上线(当前 dev 模式跳过)
  • Redis 加 requirepass
  • 日志脱敏(不打印完整 Token/Secret)

十二、常见问题 FAQ

Q1: 443 端口被占用怎么换?

sed -i 's/listen: \[":443"\]/listen: [":8443"]/' caddy.yaml && docker compose restart caddy。然后客户端 URL 加端口号 wss://域名:8443

Q2: 不用域名直接用 IP 行吗?

行。用内网模式 docker compose -f docker-compose-intranet.yaml up -d,连接 ws://IP:7880。但 WebRTC 需要 TLS 才能用麦克风,生产还是要域名。

Q3: Egress 录音为什么不启动?

三个原因:① 只有 1 人在房间 ② Redis 不通 ③ 磁盘满。

Q4: 怎么看当前有多少通话?

curl http://localhost:8080/api/health/detail | jq '.active_calls'

Q5: 录音文件在哪?

/opt/livekit/AppServer/recordings/{room_name}/{主叫}_{被叫}_{时间}.mp3

Q6: AppServer 重启在线用户会丢吗?

会。当前 ConnectionManager 是内存状态。生产建议加 Redis 持久化或客户端自动重连。

Q7: 怎么升级 LiveKit?

docker compose pull livekit && docker compose up -d livekit

Q8: 支持多少并发?

8核:6路录音 + ~200路纯通话。16核:14路录音 + ~500路纯通话。瓶颈在 Egress CPU。


附录

文件 服务器路径 用途
main.py /opt/livekit/AppServer/ AppServer 主程序
models.py /opt/livekit/AppServer/ 数据模型
config.py /opt/livekit/AppServer/ 配置读取
.env /opt/livekit/AppServer/ 密钥/数据库
docker-compose.yaml /opt/livekit/livekit.your-domain.com/ Docker 编排
livekit.yaml /opt/livekit/livekit.your-domain.com/ ★ LiveKit 配置
caddy.yaml /opt/livekit/livekit.your-domain.com/ Caddy 代理
egress.yaml /opt/livekit/livekit.your-domain.com/ Egress 录音
redis.conf /opt/livekit/livekit.your-domain.com/ Redis 配置

B. 一键部署脚本

bash 复制代码
#!/bin/bash
# deploy.sh --- LiveKit 一键部署
set -e
echo "=== LiveKit 部署开始 ==="

mkdir -p /opt/livekit/{AppServer/{logs,recordings},livekit.your-domain.com}
cd /opt/livekit

# 拉取配置生成器并生成配置
docker pull livekit/generate
docker run --rm -v $PWD:/output livekit/generate --local

# 启动所有服务
cd livekit.your-domain.com
docker compose pull
docker compose up -d

# 健康检查
sleep 3
curl -s http://127.0.0.1:7880 && echo " LiveKit ✅"
docker compose ps
echo "=== 部署完成 ==="

C. 参考资料

编号 文献 链接 说明
1 LiveKit 官方文档 https://docs.livekit.io/ 快速开始、配置参考、API 文档
2 LiveKit Server GitHub https://github.com/livekit/livekit 服务端源码、SDK 生态说明
3 LiveKit 部署指南 https://docs.livekit.io/transport/self-hosting/ 自托管部署完整步骤
4 LiveKit CLI 工具 https://github.com/livekit/livekit-cli 命令行 Token 签发与测试
5 Caddy Layer4 代理 https://caddyserver.com/docs/modules/layer4 L4 层 TLS 终结配置
6 WebRTC W3C 标准 https://www.w3.org/TR/webrtc/ 浏览器实时通信规范
7 TURN/STUN 协议 https://datatracker.ietf.org/doc/html/rfc8656 NAT 穿透技术标准
8 Let's Encrypt 证书 https://letsencrypt.org/docs/ 免费 TLS 证书自动签发
9 Docker 官方文档 https://docs.docker.com/ 容器运行时与 Compose
10 FastAPI WebSocket https://fastapi.tiangolo.com/advanced/websockets/ Python 异步 WebSocket
11 LiveKit Client SDK Flutter https://github.com/livekit/client-sdk-flutter Flutter 端媒体 SDK
12 LiveKit Server SDK Python https://github.com/livekit/python-sdks Python 后端 Token/API SDK

互动 & 推荐阅读

投票:你觉得这篇部署文档对你有帮助吗?

  • 👍 非常有用,照着部署一次成功
  • ✅ 有用,解决了一些卡点
  • 📖 作为参考资料查阅还不错
  • 💬 有疑问,评论区交流

推荐阅读(内部链接)

原创声明

本文为作者原创,基于 LiveKit v1.13.2 实际部署验证。文中所有代码均可直接执行,配置已脱敏处理。适用环境:Linux (CentOS 7+/Ubuntu 20.04+),Python 3.11+,Docker 24.0+。

📅 适用版本 : LiveKit Server v1.13.2 ~ latest | 最后验证: 2026-07-01