OpenIM + LiveKit 内网测试环境一键部署包
- docker-compose: openim-server v3.8.3-patch.12 + mongo/redis/etcd/kafka/minio + livekit v1.13.5 - 按约束移除 openim-chat(GPLv3) 及依赖它的 web/admin 前端; 监控组件收进 profile m - scripts/deploy.sh 一键部署, scripts/selftest.sh 自测(注册/登录/文字/语音/文件/一对一通话), scripts/restart.sh 重启 - README: 部署记录(服务器要求/端口表/运维/接入参数)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# ===== 服务器基础配置 =====
|
||||
# 服务器内网 IP。scripts/deploy.sh 会自动探测并写入 .env,也可手动改。
|
||||
SERVER_IP=127.0.0.1
|
||||
|
||||
# ===== 镜像版本 =====
|
||||
MONGO_IMAGE=mongo:7.0
|
||||
REDIS_IMAGE=redis:7.0.0
|
||||
KAFKA_IMAGE=bitnamilegacy/kafka:3.5.1
|
||||
MINIO_IMAGE=minio/minio:RELEASE.2024-01-11T07-46-16Z
|
||||
ETCD_IMAGE=bitnamilegacy/etcd:3.5.13
|
||||
OPENIM_SERVER_IMAGE=openim/openim-server:v3.8.3-patch.12
|
||||
LIVEKIT_IMAGE=livekit/livekit-server:v1.13.5
|
||||
# 国内拉镜像慢时,OpenIM 可换阿里云镜像:
|
||||
#OPENIM_SERVER_IMAGE=registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-server:release-v3.8.3
|
||||
|
||||
# 监控组件(可选,仅 docker compose --profile m up -d 时启动)
|
||||
PROMETHEUS_IMAGE=prom/prometheus:v2.51.2
|
||||
ALERTMANAGER_IMAGE=prom/alertmanager:v0.27.0
|
||||
GRAFANA_IMAGE=grafana/grafana:11.0.1
|
||||
NODE_EXPORTER_IMAGE=prom/node-exporter:v1.7.0
|
||||
|
||||
# ===== 数据目录(相对 docker-compose.yaml 所在目录)=====
|
||||
DATA_DIR=./
|
||||
|
||||
# ===== MongoDB =====
|
||||
MONGO_ADDRESS="mongo:27017"
|
||||
MONGO_USERNAME=openIM
|
||||
MONGO_PASSWORD=openIM123
|
||||
|
||||
# ===== Kafka =====
|
||||
KAFKA_ADDRESS="kafka:9094"
|
||||
# 默认不启用 Kafka 认证;如需启用取消下面两行注释(启动时的缺变量警告可忽略)
|
||||
#KAFKA_USERNAME=openIM
|
||||
#KAFKA_PASSWORD=openIM123
|
||||
|
||||
# ===== Etcd =====
|
||||
ETCD_ADDRESS="etcd:2379"
|
||||
# 默认不启用 Etcd 认证;如需启用取消下面四行注释
|
||||
#ETCD_ROOT_USER=root
|
||||
#ETCD_ROOT_PASSWORD=openIM123
|
||||
#ETCD_USERNAME=openIM
|
||||
#ETCD_PASSWORD=openIM123
|
||||
|
||||
# ===== Redis =====
|
||||
REDIS_ADDRESS="redis:6379"
|
||||
REDIS_PASSWORD=openIM123
|
||||
|
||||
# ===== MinIO(图片/语音/文件存储)=====
|
||||
# 关键:必须是客户端(手机/PC)能访问到的地址,deploy.sh 会写入 http://<SERVER_IP>:10005
|
||||
MINIO_EXTERNAL_ADDRESS="http://127.0.0.1:10005"
|
||||
MINIO_INTERNAL_ADDRESS="minio:9000"
|
||||
MINIO_ACCESS_KEY_ID=root
|
||||
MINIO_SECRET_ACCESS_KEY=openIM123
|
||||
MINIO_PORT=10005
|
||||
MINIO_CONSOLE_PORT=10004
|
||||
|
||||
# ===== OpenIM 服务端 =====
|
||||
OPENIM_SECRET=openIM123
|
||||
OPENIM_API_PORT=10002
|
||||
OPENIM_MSG_GATEWAY_PORT=10001
|
||||
LOG_IS_STDOUT=true
|
||||
LOG_LEVEL=3
|
||||
|
||||
# ===== LiveKit(一对一语音通话)=====
|
||||
LIVEKIT_API_KEY=openimLKkey
|
||||
LIVEKIT_API_SECRET=openimLKsecretChangeMe123
|
||||
LIVEKIT_PORT=7880
|
||||
|
||||
# ===== 监控端口(可选)=====
|
||||
PROMETHEUS_PORT=19090
|
||||
ALERTMANAGER_PORT=19093
|
||||
GRAFANA_PORT=13000
|
||||
NODE_EXPORTER_PORT=19100
|
||||
GRAFANA_URL="http://127.0.0.1:13000/"
|
||||
@@ -0,0 +1,4 @@
|
||||
# 运行时生成,不入库
|
||||
.env
|
||||
components/
|
||||
.selftest/
|
||||
@@ -0,0 +1,88 @@
|
||||
# OpenIM + LiveKit 内网测试环境部署记录
|
||||
|
||||
内部通讯 App 的服务端测试环境。一键部署后两个测试账号可互发文字/语音/文件消息,并可打通一对一语音通话。
|
||||
|
||||
## 组件清单
|
||||
|
||||
| 组件 | 镜像 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| OpenIM Server | `openim/openim-server:v3.8.3-patch.12` | IM 服务端(REST API + WebSocket 消息网关) |
|
||||
| LiveKit | `livekit/livekit-server:v1.13.5` | 语音通话 SFU |
|
||||
| MongoDB / Redis / Etcd / Kafka / MinIO | 见 `.env.example` | OpenIM 依赖组件 |
|
||||
|
||||
按任务约束做的裁剪(相对官方 openim-docker v3.8):
|
||||
|
||||
- 移除 `openim-chat`(GPLv3 授权不利改造,注册登录后续由"公司账号登录"卡自研)
|
||||
- 移除 `openim-web-front` / `openim-admin-front`(这两个前端依赖 chat 服务)
|
||||
- 监控组件(Prometheus/Grafana 等)保留在 compose profile `m` 下,默认不启动
|
||||
|
||||
未改动 OpenIM 服务端任何核心代码,全部通过环境变量配置(`IMENV_*`)。
|
||||
|
||||
## 服务器要求
|
||||
|
||||
- 内网闲置机器,4 核 8G 起步,磁盘 ≥ 20G
|
||||
- 已安装 Docker(含 compose 插件,`docker compose version` 能跑通即可)
|
||||
- 不需要公网入口
|
||||
|
||||
## 端口表(需要对客户端开放的只有前 5 个)
|
||||
|
||||
| 端口 | 协议 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 10001 | TCP/WS | OpenIM 消息网关(客户端长连接) |
|
||||
| 10002 | TCP/HTTP | OpenIM REST API |
|
||||
| 10005 | TCP/HTTP | MinIO(图片/语音/文件下载) |
|
||||
| 7880 | TCP/WS | LiveKit 信令 |
|
||||
| 7882 | UDP | LiveKit 语音媒体 |
|
||||
| 7881 | TCP | LiveKit 媒体备用通道(UDP 不通时) |
|
||||
| 10004 | TCP/HTTP | MinIO 控制台(运维用,可不开放) |
|
||||
| 12379/12380 | TCP | Etcd(仅容器间用,建议不对外) |
|
||||
|
||||
## 部署步骤
|
||||
|
||||
```bash
|
||||
git clone ssh://git@192.168.200.36:222/leefer/tongxunruanjian.git
|
||||
cd tongxunruanjian
|
||||
./scripts/deploy.sh # 自动探测本机内网 IP;也可 ./scripts/deploy.sh 192.168.x.x 指定
|
||||
./scripts/selftest.sh # 自测(见下)
|
||||
```
|
||||
|
||||
`deploy.sh` 做的事:首次从 `.env.example` 生成 `.env` → 把服务器 IP 写入 `MINIO_EXTERNAL_ADDRESS`(文件下载链接用这个地址,写错会导致文件发不出)→ 预建数据目录 → `docker compose pull && up -d` → 等待 openim-server 健康检查通过。
|
||||
|
||||
数据全部落在仓库目录下的 `components/` 里;`.env` 里的密码/密钥是测试环境默认值,正式环境前再换。
|
||||
|
||||
## 自测覆盖(scripts/selftest.sh)
|
||||
|
||||
1. 管理员令牌获取(`/auth/get_admin_token`)
|
||||
2. 注册测试账号 `test001` / `test002`(`/user/user_register`)
|
||||
3. 签发用户令牌,即登录链路(`/auth/get_user_token`)
|
||||
4. 两账号互发文字消息(`/msg/send_msg`)
|
||||
5. 附件上传 MinIO 并发送语音消息、文件消息
|
||||
6. 消息落库回查(`/msg/search_msg`,逐条核对刚发的文字/语音/文件)
|
||||
7. LiveKit 一对一:两个客户端进同一房间,主叫发布音频轨,被叫端确认收到轨道
|
||||
|
||||
全部通过会打印客户端接入参数。失败项会标 `[失败]`,配合 `docker compose logs -f openim-server` 或 `docker compose logs -f livekit` 排查。
|
||||
|
||||
## 日常运维
|
||||
|
||||
```bash
|
||||
./scripts/restart.sh # 重启全部服务
|
||||
./scripts/restart.sh livekit # 只重启某个服务
|
||||
docker compose ps # 查看状态
|
||||
docker compose logs -f openim-server # 看日志
|
||||
docker compose down # 停止(数据保留在 components/)
|
||||
docker compose up -d # 再启动
|
||||
```
|
||||
|
||||
## 客户端接入参数(自测通过后)
|
||||
|
||||
- OpenIM API: `http://<服务器IP>:10002`
|
||||
- OpenIM WebSocket: `ws://<服务器IP>:10001`
|
||||
- LiveKit: `ws://<服务器IP>:7880`,API Key/Secret 见 `.env` 的 `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`
|
||||
- 测试账号:`test001` / `test002`(由自测脚本注册);语音通话时两名用户以各自 identity 进同一房间即可
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **文件/语音能发但打不开**:`MINIO_EXTERNAL_ADDRESS` 没写成客户端能访问的 IP。改 `.env` 后 `docker compose up -d` 重建 openim-server。
|
||||
- **启动报缺 `ETCD_USERNAME` / `KAFKA_USERNAME` 等警告**:未启用对应组件认证,官方说明可忽略。
|
||||
- **openim-server 一直 unhealthy**:`docker exec -it openim-server mage check` 看哪项依赖没通;首次启动需等 30-60 秒。
|
||||
- **语音通话连不上**:确认服务器 UDP 7882 放行(内网防火墙/安全组);UDP 不通时会走 TCP 7881 兜底。
|
||||
@@ -0,0 +1,34 @@
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
smtp_from: alert@openim.io
|
||||
smtp_smarthost: smtp.163.com:465
|
||||
smtp_auth_username: alert@openim.io
|
||||
smtp_auth_password: YOURAUTHPASSWORD
|
||||
smtp_require_tls: false
|
||||
smtp_hello: xxx
|
||||
|
||||
templates:
|
||||
- /etc/alertmanager/email.tmpl
|
||||
|
||||
route:
|
||||
group_by: [ 'alertname' ]
|
||||
group_wait: 5s
|
||||
group_interval: 5s
|
||||
repeat_interval: 5m
|
||||
receiver: email
|
||||
routes:
|
||||
- matchers:
|
||||
- alertname = "XXX"
|
||||
group_by: [ 'instance' ]
|
||||
group_wait: 5s
|
||||
group_interval: 5s
|
||||
repeat_interval: 5m
|
||||
receiver: email
|
||||
|
||||
receivers:
|
||||
- name: email
|
||||
email_configs:
|
||||
- to: 'alert@example.com'
|
||||
html: '{{ template "email.to.html" . }}'
|
||||
headers: { Subject: "[OPENIM-SERVER]Alarm" }
|
||||
send_resolved: true
|
||||
@@ -0,0 +1,36 @@
|
||||
{{ define "email.to.html" }}
|
||||
{{ if eq .Status "firing" }}
|
||||
{{ range .Alerts }}
|
||||
<!-- Begin of OpenIM Alert -->
|
||||
<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">
|
||||
<h3>OpenIM Alert</h3>
|
||||
<p><strong>Alert Status:</strong> firing</p>
|
||||
<p><strong>Alert Program:</strong> Prometheus Alert</p>
|
||||
<p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>
|
||||
<p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>
|
||||
<p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>
|
||||
<p><strong>Affected Service:</strong> {{ .Labels.job }}</p>
|
||||
<p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>
|
||||
<p><strong>Trigger Time:</strong> {{ .StartsAt.Format "2006-01-02 15:04:05" }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ else if eq .Status "resolved" }}
|
||||
{{ range .Alerts }}
|
||||
<!-- Begin of OpenIM Alert -->
|
||||
<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">
|
||||
<h3>OpenIM Alert</h3>
|
||||
<p><strong>Alert Status:</strong> resolved</p>
|
||||
<p><strong>Alert Program:</strong> Prometheus Alert</p>
|
||||
<p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>
|
||||
<p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>
|
||||
<p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>
|
||||
<p><strong>Affected Service:</strong> {{ .Labels.job }}</p>
|
||||
<p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>
|
||||
<p><strong>Trigger Time:</strong> {{ .StartsAt.Format "2006-01-02 15:04:05" }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<!-- End of OpenIM Alert -->
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -0,0 +1,44 @@
|
||||
groups:
|
||||
- name: instance_down
|
||||
rules:
|
||||
- alert: InstanceDown
|
||||
expr: up == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Instance {{ $labels.instance }} down"
|
||||
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minutes."
|
||||
|
||||
- name: database_insert_failure_alerts
|
||||
rules:
|
||||
- alert: DatabaseInsertFailed
|
||||
expr: (increase(msg_insert_redis_failed_total[5m]) > 0) or (increase(msg_insert_mongo_failed_total[5m]) > 0)
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Increase in MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter detected"
|
||||
description: "Either MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter has increased in the last 5 minutes, indicating failures in message insert operations to Redis or MongoDB,maybe the redis or mongodb is crash."
|
||||
|
||||
- name: registrations_few
|
||||
rules:
|
||||
- alert: RegistrationsFew
|
||||
expr: increase(user_login_total[1h]) == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: info
|
||||
annotations:
|
||||
summary: "Too few registrations within the time frame"
|
||||
description: "The number of registrations in the last hour is 0. There might be some issues."
|
||||
|
||||
- name: messages_few
|
||||
rules:
|
||||
- alert: MessagesFew
|
||||
expr: (increase(single_chat_msg_process_success_total[1h])+increase(group_chat_msg_process_success_total[1h])) == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: info
|
||||
annotations:
|
||||
summary: "Too few messages within the time frame"
|
||||
description: "The number of messages sent in the last hour is 0. There might be some issues."
|
||||
@@ -0,0 +1,15 @@
|
||||
# LiveKit 服务端配置(内网部署)
|
||||
# API Key/Secret 通过环境变量 LIVEKIT_KEYS 注入(见 docker-compose.yaml)
|
||||
# 服务器对内 IP 通过启动参数 --node-ip 注入(deploy.sh 写入 .env 的 SERVER_IP)
|
||||
port: 7880
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
udp_port: 7882
|
||||
# 内网部署:不做公网 IP 探测,candidate 直接使用 --node-ip
|
||||
use_external_ip: false
|
||||
room:
|
||||
# 房间内最后一人离开后保留 5 分钟再销毁,避免短暂掉线房间即失效
|
||||
departure_timeout: 300
|
||||
empty_timeout: 300
|
||||
logging:
|
||||
level: info
|
||||
@@ -0,0 +1,119 @@
|
||||
# my global config
|
||||
global:
|
||||
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
|
||||
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
# Alertmanager configuration
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets: [127.0.0.1:19093]
|
||||
|
||||
# Load rules once and periodically evaluate them according to the global evaluation_interval.
|
||||
rule_files:
|
||||
- instance-down-rules.yml
|
||||
# - first_rules.yml
|
||||
# - second_rules.yml
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape:
|
||||
# Here it's Prometheus itself.
|
||||
scrape_configs:
|
||||
# The job name is added as a label "job=job_name" to any timeseries scraped from this config.
|
||||
# Monitored information captured by prometheus
|
||||
|
||||
# prometheus fetches application services
|
||||
- job_name: node_exporter
|
||||
static_configs:
|
||||
- targets: [ 127.0.0.1:19100 ]
|
||||
|
||||
- job_name: openimserver-openim-api
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/api"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12002 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-msggateway
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/msg_gateway"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12140 ]
|
||||
# # - targets: [ 127.0.0.1:12140, 127.0.0.1:12141, 127.0.0.1:12142, 127.0.0.1:12143, 127.0.0.1:12144, 127.0.0.1:12145, 127.0.0.1:12146, 127.0.0.1:12147, 127.0.0.1:12148, 127.0.0.1:12149, 127.0.0.1:12150, 127.0.0.1:12151, 127.0.0.1:12152, 127.0.0.1:12153, 127.0.0.1:12154, 127.0.0.1:12155 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-msgtransfer
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/msg_transfer"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027 ]
|
||||
# # - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027, 127.0.0.1:12028, 127.0.0.1:12029, 127.0.0.1:12030, 127.0.0.1:12031, 127.0.0.1:12032, 127.0.0.1:12033, 127.0.0.1:12034, 127.0.0.1:12035 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-push
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/push"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177 ]
|
||||
## - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177, 127.0.0.1:12178, 127.0.0.1:12179, 127.0.0.1:12180, 127.0.0.1:12182, 127.0.0.1:12183, 127.0.0.1:12184, 127.0.0.1:12185, 127.0.0.1:12186 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-auth
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/auth"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12200 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-conversation
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/conversation"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12220 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-friend
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/friend"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12240 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-group
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/group"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12260 ]
|
||||
# labels:
|
||||
# namespace: default.
|
||||
|
||||
- job_name: openimserver-openim-rpc-msg
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/msg"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12280 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-third
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/third"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12300 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
|
||||
- job_name: openimserver-openim-rpc-user
|
||||
http_sd_configs:
|
||||
- url: "http://127.0.0.1:10002/prometheus_discovery/user"
|
||||
# static_configs:
|
||||
# - targets: [ 127.0.0.1:12320 ]
|
||||
# labels:
|
||||
# namespace: default
|
||||
@@ -0,0 +1,387 @@
|
||||
# 内部通讯 App —— OpenIM + LiveKit 内网测试环境
|
||||
# 基于官方 openim-docker v3.8 裁剪:
|
||||
# - 移除 openim-chat(GPLv3,本项目的注册登录后续自研)
|
||||
# - 移除 openim-web-front / openim-admin-front(均依赖 chat 服务)
|
||||
# - 新增 livekit 服务(语音通话 SFU)
|
||||
# 监控组件(prometheus/alertmanager/grafana/node-exporter)保留在 profile "m" 下,
|
||||
# 默认不启动;需要时执行 docker compose --profile m up -d。
|
||||
networks:
|
||||
openim:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: "${MONGO_IMAGE}"
|
||||
container_name: mongo
|
||||
command: >
|
||||
bash -c '
|
||||
docker-entrypoint.sh mongod --wiredTigerCacheSizeGB $$wiredTigerCacheSizeGB --auth &
|
||||
until mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval "db.runCommand({ ping: 1 })" &>/dev/null; do
|
||||
echo "Waiting for MongoDB to start..."
|
||||
sleep 1
|
||||
done &&
|
||||
mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval "
|
||||
db = db.getSiblingDB(\"$$MONGO_INITDB_DATABASE\");
|
||||
if (!db.getUser(\"$$MONGO_OPENIM_USERNAME\")) {
|
||||
db.createUser({
|
||||
user: \"$$MONGO_OPENIM_USERNAME\",
|
||||
pwd: \"$$MONGO_OPENIM_PASSWORD\",
|
||||
roles: [{role: \"readWrite\", db: \"$$MONGO_INITDB_DATABASE\"}]
|
||||
});
|
||||
print(\"User created successfully: \");
|
||||
print(\"Username: $$MONGO_OPENIM_USERNAME\");
|
||||
print(\"Password: $$MONGO_OPENIM_PASSWORD\");
|
||||
print(\"Database: $$MONGO_INITDB_DATABASE\");
|
||||
} else {
|
||||
print(\"User already exists in database: $$MONGO_INITDB_DATABASE, Username: $$MONGO_OPENIM_USERNAME\");
|
||||
}
|
||||
" &&
|
||||
tail -f /dev/null
|
||||
'
|
||||
volumes:
|
||||
- "${DATA_DIR}/components/mongodb/data/db:/data/db"
|
||||
- "${DATA_DIR}/components/mongodb/data/logs:/data/logs"
|
||||
- "${DATA_DIR}/components/mongodb/data/conf:/etc/mongo"
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- wiredTigerCacheSizeGB=1
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=openIM123
|
||||
- MONGO_INITDB_DATABASE=openim_v3
|
||||
- MONGO_OPENIM_USERNAME=${MONGO_USERNAME}
|
||||
- MONGO_OPENIM_PASSWORD=${MONGO_PASSWORD}
|
||||
restart: always
|
||||
networks:
|
||||
- openim
|
||||
|
||||
redis:
|
||||
image: "${REDIS_IMAGE}"
|
||||
container_name: redis
|
||||
volumes:
|
||||
- "${DATA_DIR}/components/redis/data:/data"
|
||||
- "${DATA_DIR}/components/redis/config/redis.conf:/usr/local/redis/config/redis.conf"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
restart: always
|
||||
sysctls:
|
||||
net.core.somaxconn: 1024
|
||||
command:
|
||||
[
|
||||
"redis-server",
|
||||
"/usr/local/redis/config/redis.conf",
|
||||
"--requirepass",
|
||||
"${REDIS_PASSWORD}",
|
||||
"--appendonly",
|
||||
"yes",
|
||||
]
|
||||
networks:
|
||||
- openim
|
||||
|
||||
etcd:
|
||||
image: "${ETCD_IMAGE}"
|
||||
container_name: etcd
|
||||
ports:
|
||||
- "12379:2379"
|
||||
- "12380:2380"
|
||||
environment:
|
||||
- ETCD_NAME=s1
|
||||
- ETCD_DATA_DIR=/etcd-data
|
||||
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
|
||||
- ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
|
||||
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
|
||||
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://0.0.0.0:2380
|
||||
- ETCD_INITIAL_CLUSTER=s1=http://0.0.0.0:2380
|
||||
- ETCD_INITIAL_CLUSTER_TOKEN=tkn
|
||||
- ETCD_INITIAL_CLUSTER_STATE=new
|
||||
- ALLOW_NONE_AUTHENTICATION=no
|
||||
|
||||
## Optional: Enable etcd authentication by setting the following credentials
|
||||
# - ETCD_ROOT_USER=root
|
||||
# - ETCD_ROOT_PASSWORD=openIM123
|
||||
# - ETCD_USERNAME=openIM
|
||||
# - ETCD_PASSWORD=openIM123
|
||||
volumes:
|
||||
- "${DATA_DIR}/components/etcd:/etcd-data"
|
||||
command: >
|
||||
/bin/sh -c '
|
||||
etcd &
|
||||
export ETCDCTL_API=3
|
||||
echo "Waiting for etcd to become healthy..."
|
||||
until etcdctl --endpoints=http://127.0.0.1:2379 endpoint health &>/dev/null; do
|
||||
echo "Waiting for ETCD to start..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "etcd is healthy."
|
||||
|
||||
if [ -n "$${ETCD_ROOT_USER}" ] && [ -n "$${ETCD_ROOT_PASSWORD}" ] && [ -n "$${ETCD_USERNAME}" ] && [ -n "$${ETCD_PASSWORD}" ]; then
|
||||
echo "Authentication credentials provided. Setting up authentication..."
|
||||
|
||||
echo "Checking authentication status..."
|
||||
if ! etcdctl --endpoints=http://127.0.0.1:2379 auth status | grep -q "Authentication Status: true"; then
|
||||
echo "Authentication is disabled. Creating users and enabling..."
|
||||
|
||||
# Create users and setup permissions
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_ROOT_USER} --new-user-password=$${ETCD_ROOT_PASSWORD} || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} || true
|
||||
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 role add openim-role || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite / || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite "" || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_USERNAME} openim-role || true
|
||||
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true
|
||||
|
||||
echo "Enabling authentication..."
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 auth enable
|
||||
echo "Authentication enabled successfully"
|
||||
else
|
||||
echo "Authentication is already enabled. Checking OpenIM user..."
|
||||
|
||||
# Check if openIM user exists and can perform operations
|
||||
if ! etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-check" &>/dev/null; then
|
||||
echo "OpenIM user test failed. Recreating user with root credentials..."
|
||||
|
||||
# Try to create/update the openIM user using root credentials
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} --no-password-file || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role add openim-role || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite / || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite "" || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user grant-role $${ETCD_USERNAME} openim-role || true
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true
|
||||
|
||||
echo "OpenIM user recreated with required permissions"
|
||||
else
|
||||
echo "OpenIM user exists and has correct permissions"
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth &>/dev/null
|
||||
fi
|
||||
fi
|
||||
echo "Testing authentication with OpenIM user..."
|
||||
if etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-works"; then
|
||||
echo "Authentication working properly"
|
||||
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth
|
||||
else
|
||||
echo "WARNING: Authentication test failed"
|
||||
fi
|
||||
else
|
||||
echo "No authentication credentials provided. Running in no-auth mode."
|
||||
echo "To enable authentication, set ETCD_ROOT_USER, ETCD_ROOT_PASSWORD, ETCD_USERNAME, and ETCD_PASSWORD environment variables."
|
||||
fi
|
||||
|
||||
tail -f /dev/null
|
||||
'
|
||||
restart: always
|
||||
networks:
|
||||
- openim
|
||||
|
||||
kafka:
|
||||
image: "${KAFKA_IMAGE}"
|
||||
container_name: kafka
|
||||
user: root
|
||||
restart: always
|
||||
volumes:
|
||||
- "${DATA_DIR}/components/kafka:/bitnami/kafka"
|
||||
environment:
|
||||
#KAFKA_HEAP_OPTS: "-Xms128m -Xmx256m"
|
||||
TZ: Asia/Shanghai
|
||||
# Unique identifier for the Kafka node (required in controller mode)
|
||||
KAFKA_CFG_NODE_ID: 0
|
||||
# Defines the roles this Kafka node plays: broker, controller, or both
|
||||
KAFKA_CFG_PROCESS_ROLES: controller,broker
|
||||
# Specifies which nodes are controller nodes for quorum voting.
|
||||
# The syntax follows the KRaft mode (no ZooKeeper): node.id@host:port
|
||||
# The controller listener endpoint here is kafka:9093
|
||||
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
||||
# Specifies which listener is used for controller-to-controller communication
|
||||
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
||||
# Default number of partitions for new topics
|
||||
KAFKA_NUM_PARTITIONS: 8
|
||||
# Whether to enable automatic topic creation
|
||||
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
# Kafka internal listeners; Kafka supports multiple ports with different protocols
|
||||
# Each port is used for a specific purpose: INTERNAL for internal broker communication,
|
||||
# CONTROLLER for controller communication, EXTERNAL for external client connections.
|
||||
# These logical listener names are mapped to actual protocols via KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP
|
||||
# In short, Kafka is listening on three logical ports: 9092 for internal communication,
|
||||
# 9093 for controller traffic, and 9094 for external access.
|
||||
KAFKA_CFG_LISTENERS: "INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094"
|
||||
# Addresses advertised to clients. INTERNAL://kafka:9092 uses the internal Docker service name 'kafka',
|
||||
# so other containers can access Kafka via kafka:9092.
|
||||
# EXTERNAL://localhost:19094 is the address external clients (e.g., in the LAN) should use to connect.
|
||||
# If Kafka is deployed on a different machine than IM, 'localhost'(or 'kafka') should be replaced with the LAN IP.
|
||||
KAFKA_CFG_ADVERTISED_LISTENERS: "INTERNAL://kafka:9092,EXTERNAL://kafka:9094"
|
||||
# Maps logical listener names to actual protocols.
|
||||
# Supported protocols include: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
|
||||
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT"
|
||||
# Defines which listener is used for inter-broker communication within the Kafka cluster
|
||||
KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "INTERNAL"
|
||||
|
||||
# Authentication configuration variables - comment out to disable auth
|
||||
# KAFKA_USERNAME: "openIM"
|
||||
# KAFKA_PASSWORD: "openIM123"
|
||||
command: >
|
||||
/bin/sh -c '
|
||||
if [ -n "$${KAFKA_USERNAME}" ] && [ -n "$${KAFKA_PASSWORD}" ]; then
|
||||
echo "=== Kafka SASL Authentication ENABLED ==="
|
||||
echo "Username: $${KAFKA_USERNAME}"
|
||||
|
||||
# Set environment variables for SASL authentication
|
||||
export KAFKA_CFG_LISTENERS="SASL_PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094"
|
||||
export KAFKA_CFG_ADVERTISED_LISTENERS="SASL_PLAINTEXT://kafka:9092,EXTERNAL://kafka:9094"
|
||||
export KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP="CONTROLLER:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT"
|
||||
export KAFKA_CFG_SASL_ENABLED_MECHANISMS="PLAIN"
|
||||
export KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL="PLAIN"
|
||||
export KAFKA_CFG_INTER_BROKER_LISTENER_NAME="SASL_PLAINTEXT"
|
||||
export KAFKA_CLIENT_USERS="$${KAFKA_USERNAME}"
|
||||
export KAFKA_CLIENT_PASSWORDS="$${KAFKA_PASSWORD}"
|
||||
fi
|
||||
|
||||
# Start Kafka with the configured environment
|
||||
exec /opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh
|
||||
'
|
||||
networks:
|
||||
- openim
|
||||
|
||||
minio:
|
||||
image: "${MINIO_IMAGE}"
|
||||
ports:
|
||||
- "${MINIO_PORT}:9000"
|
||||
- "${MINIO_CONSOLE_PORT}:9090"
|
||||
container_name: minio
|
||||
volumes:
|
||||
- "${DATA_DIR}/components/mnt/data:/data"
|
||||
- "${DATA_DIR}/components/mnt/config:/root/.minio"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
MINIO_ROOT_USER: "${MINIO_ACCESS_KEY_ID}"
|
||||
MINIO_ROOT_PASSWORD: "${MINIO_SECRET_ACCESS_KEY}"
|
||||
restart: always
|
||||
command: minio server /data --console-address ':9090'
|
||||
networks:
|
||||
- openim
|
||||
|
||||
prometheus:
|
||||
image: ${PROMETHEUS_IMAGE}
|
||||
container_name: prometheus
|
||||
restart: always
|
||||
user: root
|
||||
profiles:
|
||||
- m
|
||||
volumes:
|
||||
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- ./config/instance-down-rules.yml:/etc/prometheus/instance-down-rules.yml
|
||||
- ${DATA_DIR}/components/prometheus/data:/prometheus
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.listen-address=:${PROMETHEUS_PORT}"
|
||||
network_mode: host
|
||||
|
||||
alertmanager:
|
||||
image: ${ALERTMANAGER_IMAGE}
|
||||
container_name: alertmanager
|
||||
restart: always
|
||||
profiles:
|
||||
- m
|
||||
volumes:
|
||||
- ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml
|
||||
- ./config/email.tmpl:/etc/alertmanager/email.tmpl
|
||||
command:
|
||||
- "--config.file=/etc/alertmanager/alertmanager.yml"
|
||||
- "--web.listen-address=:${ALERTMANAGER_PORT}"
|
||||
network_mode: host
|
||||
|
||||
grafana:
|
||||
image: ${GRAFANA_IMAGE}
|
||||
container_name: grafana
|
||||
user: root
|
||||
restart: always
|
||||
profiles:
|
||||
- m
|
||||
environment:
|
||||
- GF_SECURITY_ALLOW_EMBEDDING=true
|
||||
- GF_SESSION_COOKIE_SAMESITE=none
|
||||
- GF_SESSION_COOKIE_SECURE=true
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
||||
- GF_SERVER_HTTP_PORT=${GRAFANA_PORT}
|
||||
volumes:
|
||||
- ${DATA_DIR:-./}/components/grafana:/var/lib/grafana
|
||||
network_mode: host
|
||||
|
||||
node-exporter:
|
||||
image: ${NODE_EXPORTER_IMAGE}
|
||||
container_name: node-exporter
|
||||
restart: always
|
||||
profiles:
|
||||
- m
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- "--path.procfs=/host/proc"
|
||||
- "--path.sysfs=/host/sys"
|
||||
- "--path.rootfs=/rootfs"
|
||||
- "--web.listen-address=:${NODE_EXPORTER_PORT}"
|
||||
network_mode: host
|
||||
|
||||
openim-server:
|
||||
image: ${OPENIM_SERVER_IMAGE}
|
||||
container_name: openim-server
|
||||
init: true
|
||||
ports:
|
||||
- "${OPENIM_MSG_GATEWAY_PORT}:10001"
|
||||
- "${OPENIM_API_PORT}:10002"
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "mage check"]
|
||||
interval: 5s
|
||||
timeout: 60s
|
||||
retries: 10
|
||||
environment:
|
||||
- IMENV_MONGODB_ADDRESS=${MONGO_ADDRESS}
|
||||
- IMENV_MONGODB_USERNAME=${MONGO_USERNAME}
|
||||
- IMENV_MONGODB_PASSWORD=${MONGO_PASSWORD}
|
||||
- IMENV_KAFKA_ADDRESS=${KAFKA_ADDRESS}
|
||||
- IMENV_KAFKA_USERNAME=${KAFKA_USERNAME}
|
||||
- IMENV_KAFKA_PASSWORD=${KAFKA_PASSWORD}
|
||||
- IMENV_DISCOVERY_ETCD_ADDRESS=${ETCD_ADDRESS}
|
||||
- IMENV_REDIS_ADDRESS=${REDIS_ADDRESS}
|
||||
- IMENV_REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- IMENV_DISCOVERY_ETCD_USERNAME=${ETCD_USERNAME}
|
||||
- IMENV_DISCOVERY_ETCD_PASSWORD=${ETCD_PASSWORD}
|
||||
- IMENV_MINIO_INTERNALADDRESS=${MINIO_INTERNAL_ADDRESS}
|
||||
- IMENV_MINIO_EXTERNALADDRESS=${MINIO_EXTERNAL_ADDRESS}
|
||||
- IMENV_MINIO_ACCESSKEYID=${MINIO_ACCESS_KEY_ID}
|
||||
- IMENV_MINIO_SECRETACCESSKEY=${MINIO_SECRET_ACCESS_KEY}
|
||||
- IMENV_SHARE_SECRET=${OPENIM_SECRET}
|
||||
- IMENV_LOG_ISSTDOUT=${LOG_IS_STDOUT}
|
||||
- IMENV_LOG_REMAINLOGLEVEL=${LOG_LEVEL}
|
||||
- IMENV_OPENIM_API_PROMETHEUS_GRAFANAURL=${GRAFANA_URL}
|
||||
restart: always
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
- etcd
|
||||
- kafka
|
||||
- minio
|
||||
networks:
|
||||
- openim
|
||||
|
||||
# LiveKit:语音/视频通话的 SFU,Apache 2.0,自部署。
|
||||
# 客户端通过 ws://<SERVER_IP>:7880 连接,凭 LIVEKIT_API_KEY/SECRET 签发的 token 进房。
|
||||
livekit:
|
||||
image: ${LIVEKIT_IMAGE}
|
||||
container_name: livekit
|
||||
restart: always
|
||||
command: --config /etc/livekit.yaml --node-ip ${SERVER_IP}
|
||||
environment:
|
||||
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
|
||||
ports:
|
||||
- "${LIVEKIT_PORT}:7880" # HTTP / WebSocket 信令
|
||||
- "7881:7881" # RTC over TCP(UDP 不通时的备用通道)
|
||||
- "7882:7882/udp" # RTC 媒体(UDP 复用端口)
|
||||
volumes:
|
||||
- ./config/livekit.yaml:/etc/livekit.yaml
|
||||
networks:
|
||||
- openim
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# OpenIM + LiveKit 内网测试环境一键部署
|
||||
# 用法: ./scripts/deploy.sh [服务器内网IP] (不传则自动探测)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# 1. 首次运行从模板生成 .env
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
echo "已从 .env.example 生成 .env"
|
||||
fi
|
||||
|
||||
# 2. 确定服务器内网 IP
|
||||
IP="${1:-}"
|
||||
if [ -z "$IP" ]; then
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
if [ -z "$IP" ]; then
|
||||
echo "无法自动获取内网 IP,请手动传入: ./scripts/deploy.sh <服务器IP>"
|
||||
exit 1
|
||||
fi
|
||||
echo "使用服务器 IP: $IP"
|
||||
|
||||
# 3. 把 IP 写进 .env(MinIO 外链地址必须是客户端能访问的地址)
|
||||
sed -i "s|^SERVER_IP=.*|SERVER_IP=${IP}|" .env
|
||||
sed -i "s|^MINIO_EXTERNAL_ADDRESS=.*|MINIO_EXTERNAL_ADDRESS=\"http://${IP}:10005\"|" .env
|
||||
sed -i "s|^GRAFANA_URL=.*|GRAFANA_URL=\"http://${IP}:13000/\"|" .env
|
||||
|
||||
# 4. 预创建挂载目录(避免 docker 把文件路径自动建成目录)
|
||||
mkdir -p components/redis/config components/mnt/data components/mongodb components/etcd components/kafka
|
||||
touch components/redis/config/redis.conf
|
||||
|
||||
# 5. 拉镜像并启动
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# 6. 等待 openim-server 健康(首次启动约 30-60 秒)
|
||||
echo "等待 openim-server 就绪(最长约 2 分钟)..."
|
||||
status="unknown"
|
||||
for _ in $(seq 1 24); do
|
||||
status=$(docker inspect -f '{{.State.Health.Status}}' openim-server 2>/dev/null || echo "unknown")
|
||||
[ "$status" = "healthy" ] && break
|
||||
sleep 5
|
||||
done
|
||||
|
||||
docker compose ps
|
||||
echo
|
||||
if [ "$status" = "healthy" ]; then
|
||||
echo "部署完成。下一步运行自测: ./scripts/selftest.sh ${IP}"
|
||||
else
|
||||
echo "openim-server 尚未 healthy(当前: ${status})。可执行 docker compose logs -f openim-server 查看日志。"
|
||||
fi
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# 重启测试环境。用法: ./scripts/restart.sh —— 重启全部服务
|
||||
# ./scripts/restart.sh livekit —— 只重启指定服务
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
docker compose restart "$@"
|
||||
echo "已重启。OpenIM 完全就绪约需 30-60 秒,查看状态: docker compose ps"
|
||||
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
# OpenIM + LiveKit 测试环境自测脚本
|
||||
# 覆盖:管理员令牌、注册两个测试账号、签发用户令牌(登录)、
|
||||
# 互发文字/语音/文件消息、消息回查、LiveKit 一对一房间双端进房与媒体发布订阅。
|
||||
# 用法: ./scripts/selftest.sh [服务器内网IP]
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# ---------- 参数与公共函数 ----------
|
||||
env_get() { grep -E "^$1=" .env | head -1 | cut -d= -f2- | tr -d '"'; }
|
||||
|
||||
IP="${1:-$(env_get SERVER_IP)}"
|
||||
IP="${IP:-127.0.0.1}"
|
||||
API="http://${IP}:10002"
|
||||
SECRET="$(env_get OPENIM_SECRET)"; SECRET="${SECRET:-openIM123}"
|
||||
MINIO_AK="$(env_get MINIO_ACCESS_KEY_ID)"; MINIO_AK="${MINIO_AK:-root}"
|
||||
MINIO_SK="$(env_get MINIO_SECRET_ACCESS_KEY)"; MINIO_SK="${MINIO_SK:-openIM123}"
|
||||
MINIO_PORT="$(env_get MINIO_PORT)"; MINIO_PORT="${MINIO_PORT:-10005}"
|
||||
LK_KEY="$(env_get LIVEKIT_API_KEY)"
|
||||
LK_SECRET="$(env_get LIVEKIT_API_SECRET)"
|
||||
LK_HTTP="http://${IP}:7880"
|
||||
LK_WS="ws://${IP}:7880"
|
||||
LK_CLI_IMAGE="livekit/livekit-cli:v2.18.2"
|
||||
MC_IMAGE="minio/mc:latest"
|
||||
|
||||
FAILED=0
|
||||
pass() { echo " [通过] $1"; }
|
||||
fail() { echo " [失败] $1"; FAILED=1; }
|
||||
note() { echo " [提示] $1"; }
|
||||
|
||||
# 优先用宿主机 curl;没有则在容器里跑 curl
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
http_post() { curl -s -m 15 -X POST "$1" -H 'Content-Type: application/json' ${TOKEN:+-H "token: $TOKEN"} -d "$2"; }
|
||||
else
|
||||
note "宿主机无 curl,使用容器化 curl"
|
||||
http_post() { docker run --rm --network host curlimages/curl:8.10.1 -s -m 15 -X POST "$1" -H 'Content-Type: application/json' ${TOKEN:+-H "token: $TOKEN"} -d "$2"; }
|
||||
fi
|
||||
|
||||
json_ok() { echo "$1" | tr -d ' \n' | grep -q '"errCode":0'; }
|
||||
json_field() { echo "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" | head -1; }
|
||||
|
||||
echo "==============================================="
|
||||
echo " OpenIM 自测 (API: $API)"
|
||||
echo "==============================================="
|
||||
|
||||
# ---------- 1. 管理员令牌 ----------
|
||||
echo "1. 获取管理员令牌(/auth/get_admin_token)"
|
||||
TOKEN=""
|
||||
resp=$(http_post "$API/auth/get_admin_token" "{\"secret\":\"${SECRET}\",\"userID\":\"imAdmin\"}")
|
||||
if json_ok "$resp" && [ -n "$(json_field "$resp" token)" ]; then
|
||||
TOKEN=$(json_field "$resp" token)
|
||||
pass "管理员令牌获取成功"
|
||||
else
|
||||
fail "管理员令牌获取失败: $resp"
|
||||
echo "OpenIM API 不可用,终止自测。"; exit 1
|
||||
fi
|
||||
|
||||
# ---------- 2. 注册两个测试账号 ----------
|
||||
echo "2. 注册测试账号 test001 / test002(/user/user_register)"
|
||||
resp=$(http_post "$API/user/user_register" "{\"secret\":\"${SECRET}\",\"users\":[{\"userID\":\"test001\",\"nickname\":\"测试一号\"},{\"userID\":\"test002\",\"nickname\":\"测试二号\"}]}")
|
||||
if json_ok "$resp"; then
|
||||
pass "两个测试账号注册成功"
|
||||
else
|
||||
# 重复执行脚本时账号已存在,属于正常情况
|
||||
note "注册接口返回非 0(通常是账号已存在,重复自测属正常): $(echo "$resp" | head -c 200)"
|
||||
fi
|
||||
|
||||
# ---------- 3. 签发用户令牌(登录) ----------
|
||||
echo "3. 签发用户令牌(/auth/get_user_token,platformID=5)"
|
||||
resp=$(http_post "$API/auth/get_user_token" "{\"secret\":\"${SECRET}\",\"platformID\":5,\"userID\":\"test001\"}")
|
||||
T1=$(json_field "$resp" token)
|
||||
resp=$(http_post "$API/auth/get_user_token" "{\"secret\":\"${SECRET}\",\"platformID\":5,\"userID\":\"test002\"}")
|
||||
T2=$(json_field "$resp" token)
|
||||
if [ -n "$T1" ] && [ -n "$T2" ]; then
|
||||
pass "test001 / test002 均拿到用户令牌(登录链路可用)"
|
||||
else
|
||||
fail "用户令牌签发失败"
|
||||
fi
|
||||
|
||||
# ---------- 4. 互发文字消息 ----------
|
||||
MARK="selftest-$(date +%s)"
|
||||
echo "4. test001 -> test002 发送文字消息(/msg/send_msg)"
|
||||
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"content\":\"自测文字 ${MARK}\"},\"contentType\":101,\"sessionType\":1}")
|
||||
if json_ok "$resp"; then pass "文字消息发送成功"; else fail "文字消息发送失败: $(echo "$resp" | head -c 200)"; fi
|
||||
|
||||
echo " test002 -> test001 回复文字消息"
|
||||
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test002\",\"recvID\":\"test001\",\"senderNickname\":\"测试二号\",\"senderPlatformID\":5,\"content\":{\"content\":\"收到 ${MARK}\"},\"contentType\":101,\"sessionType\":1}")
|
||||
if json_ok "$resp"; then pass "回复消息发送成功"; else fail "回复消息发送失败: $(echo "$resp" | head -c 200)"; fi
|
||||
|
||||
# ---------- 5. 上传文件到 MinIO 并发送语音/文件消息 ----------
|
||||
echo "5. 上传附件到 MinIO 并发送语音、文件消息"
|
||||
mkdir -p .selftest
|
||||
echo "openim selftest file ${MARK}" > .selftest/selftest-file.txt
|
||||
head -c 4096 /dev/urandom 2>/dev/null > .selftest/selftest-voice.amr || printf 'fake-amr-%s' "$MARK" > .selftest/selftest-voice.amr
|
||||
FILE_SIZE=$(wc -c < .selftest/selftest-file.txt | tr -d ' ')
|
||||
VOICE_SIZE=$(wc -c < .selftest/selftest-voice.amr | tr -d ' ')
|
||||
|
||||
MCHOST="http://${MINIO_AK}:${MINIO_SK}@${IP}:${MINIO_PORT}"
|
||||
mc_run() { docker run --rm --network host -v "$PWD/.selftest:/data" -e "MC_HOST_local=${MCHOST}" --entrypoint mc "$MC_IMAGE" "$@"; }
|
||||
|
||||
if mc_run cp "/data/selftest-file.txt" "local/openim/selftest/${MARK}/" >/dev/null 2>&1 \
|
||||
&& mc_run cp "/data/selftest-voice.amr" "local/openim/selftest/${MARK}/" >/dev/null 2>&1; then
|
||||
FILE_URL=$(mc_run share download --expire 168h "local/openim/selftest/${MARK}/selftest-file.txt" 2>/dev/null | grep -o 'http[^ ]*' | head -1)
|
||||
VOICE_URL=$(mc_run share download --expire 168h "local/openim/selftest/${MARK}/selftest-voice.amr" 2>/dev/null | grep -o 'http[^ ]*' | head -1)
|
||||
if [ -n "$FILE_URL" ] && [ -n "$VOICE_URL" ]; then
|
||||
pass "附件已上传 MinIO 并生成访问链接"
|
||||
|
||||
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"uuid\":\"\",\"soundPath\":\"\",\"sourceUrl\":\"${VOICE_URL}\",\"dataSize\":${VOICE_SIZE},\"duration\":3},\"contentType\":103,\"sessionType\":1}")
|
||||
if json_ok "$resp"; then pass "语音消息发送成功"; else fail "语音消息发送失败: $(echo "$resp" | head -c 200)"; fi
|
||||
|
||||
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"filePath\":\"\",\"uuid\":\"\",\"sourceUrl\":\"${FILE_URL}\",\"fileName\":\"selftest-file.txt\",\"fileSize\":${FILE_SIZE}},\"contentType\":105,\"sessionType\":1}")
|
||||
if json_ok "$resp"; then pass "文件消息发送成功"; else fail "文件消息发送失败: $(echo "$resp" | head -c 200)"; fi
|
||||
else
|
||||
fail "MinIO 生成下载链接失败"
|
||||
fi
|
||||
else
|
||||
fail "附件上传 MinIO 失败(检查 minio 容器与 ${IP}:${MINIO_PORT} 连通性)"
|
||||
fi
|
||||
|
||||
# ---------- 6. 消息回查 ----------
|
||||
echo "6. 消息落库回查(/msg/search_msg)"
|
||||
sleep 2
|
||||
resp=$(http_post "$API/msg/search_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"contentType\":0,\"sendTime\":\"\",\"sessionType\":1,\"pagination\":{\"pageNumber\":1,\"showNumber\":20}}")
|
||||
if json_ok "$resp" && echo "$resp" | grep -q "$MARK"; then
|
||||
pass "文字消息已落库可回查"
|
||||
else
|
||||
fail "消息回查未找到刚发送的内容: $(echo "$resp" | head -c 200)"
|
||||
fi
|
||||
if echo "$resp" | grep -q "selftest-voice.amr"; then pass "语音消息已落库可回查"; else fail "语音消息回查未找到"; fi
|
||||
if echo "$resp" | grep -q "selftest-file.txt"; then pass "文件消息已落库可回查"; else fail "文件消息回查未找到"; fi
|
||||
|
||||
# ---------- 7. LiveKit 一对一语音房间 ----------
|
||||
echo "==============================================="
|
||||
echo " LiveKit 自测 (信令: $LK_WS)"
|
||||
echo "==============================================="
|
||||
ROOM="selftest-$(date +%s)"
|
||||
echo "7. 两名用户进入同一房间,主叫发布音频,验证双向可见"
|
||||
|
||||
cleanup_lk() { docker rm -f lk-caller lk-callee >/dev/null 2>&1 || true; }
|
||||
trap cleanup_lk EXIT
|
||||
cleanup_lk
|
||||
|
||||
lk() { docker run --rm --network host "$LK_CLI_IMAGE" --url "$1" --api-key "$LK_KEY" --api-secret "$LK_SECRET" "${@:2}"; }
|
||||
|
||||
docker run -d --name lk-caller --network host "$LK_CLI_IMAGE" \
|
||||
--url "$LK_WS" --api-key "$LK_KEY" --api-secret "$LK_SECRET" \
|
||||
room join --identity caller --publish-demo "$ROOM" >/dev/null 2>&1
|
||||
docker run -d --name lk-callee --network host "$LK_CLI_IMAGE" \
|
||||
--url "$LK_WS" --api-key "$LK_KEY" --api-secret "$LK_SECRET" \
|
||||
room join --identity callee "$ROOM" >/dev/null 2>&1
|
||||
|
||||
sleep 10
|
||||
plist=$(lk "$LK_HTTP" room participants list "$ROOM" 2>&1)
|
||||
if echo "$plist" | grep -q "caller" && echo "$plist" | grep -q "callee"; then
|
||||
pass "双方已进入同一房间(房间: $ROOM)"
|
||||
else
|
||||
fail "房间成员不符,list 输出: $(echo "$plist" | head -c 300)"
|
||||
fi
|
||||
|
||||
# 主叫发布了媒体轨道(publish-demo 会发布音视频 demo 轨)
|
||||
if echo "$plist" | grep -qi "MICROPHONE\|AUDIO"; then
|
||||
pass "主叫音频轨道已发布"
|
||||
else
|
||||
note "未从成员列表确认音频轨道(可能输出格式差异),请人工核对: lk room participants list"
|
||||
fi
|
||||
|
||||
# 被叫端日志应出现订阅到轨道的记录
|
||||
if docker logs lk-callee 2>&1 | grep -qi "track"; then
|
||||
pass "被叫端已接收到主叫的媒体轨道(一对一语音链路打通)"
|
||||
else
|
||||
fail "被叫端日志未见轨道订阅记录: $(docker logs lk-callee 2>&1 | tail -5)"
|
||||
fi
|
||||
cleanup_lk
|
||||
trap - EXIT
|
||||
|
||||
# ---------- 汇总 ----------
|
||||
echo "==============================================="
|
||||
if [ "$FAILED" = "0" ]; then
|
||||
echo " 自测全部通过 ✔"
|
||||
echo " 客户端接入参数:"
|
||||
echo " OpenIM API : $API"
|
||||
echo " OpenIM WS : ws://${IP}:10001"
|
||||
echo " LiveKit WS : $LK_WS (key: $LK_KEY)"
|
||||
else
|
||||
echo " 自测存在失败项,请根据上方 [失败] 排查(docker compose logs -f openim-server / livekit)"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user