Files
tongxunruanjian/docker-compose.yaml
T
编码工程师andmultica-agent 5cc2685f03 fix(livekit): 统一信令/媒体端口为 17880/17881/17882,修复接听后双方网络错误
根因:LiveKit 的 config/livekit.yaml 媒体端口(7881/7882)与宿主实际映射(17881/17882)不一致,
信令走 17880 能通,但媒体被宣告到 7881/7882 连不上。

- config/livekit.yaml: port/tcp_port/udp_port -> 17880/17881/17882
- docker-compose.yaml: 容器侧端口与宿主侧一致(1788x),不再重映射
- .env.example: 默认端口改为 1788x 并注明三处需一致
- scripts/selftest.sh: LK_PORT 默认值改为 17880
- scripts/fix-livekit-ports.sh: 一键备份+对齐 .env+仅重建 livekit 的修复脚本
- README: 更新端口表、客户端接入参数与排障说明

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 00:31:08 +08:00

415 lines
17 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 内部通讯 App —— OpenIM + LiveKit 内网测试环境
# 基于官方 openim-docker v3.8 裁剪:
# - 移除 openim-chatGPLv3,本项目的注册登录后续自研)
# - 移除 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:语音/视频通话的 SFUApache 2.0,自部署。
# 客户端通过 ws://<SERVER_IP>:<LIVEKIT_PORT> 连接,凭 LIVEKIT_API_KEY/SECRET 签发的 token 进房。
# 三个端口必须与 config/livekit.yaml 里容器监听的端口完全一致(默认 17880/17881/17882),
# 即“宿主侧 = 容器侧”,不要在宿主侧做重映射;LiveKit 向客户端宣告媒体地址时只认
# config/livekit.yaml 里的端口,重映射会导致信令通、媒体不通。
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:-17880}:17880" # HTTP / WebSocket 信令
- "${LIVEKIT_RTC_TCP_PORT:-17881}:17881" # RTC over TCPUDP 不通时的备用通道)
- "${LIVEKIT_RTC_UDP_PORT:-17882}:17882/udp" # RTC 媒体(UDP 复用端口)
volumes:
- ./config/livekit.yaml:/etc/livekit.yaml
networks:
- openim
# 公司账号服务:自研工号+密码登录(替代 OpenIM 样板注册登录),含极简管理页。
# 员工客户端调 http://<SERVER_IP>:10010/api/login 拿 OpenIM token
# 管理员开 http://<SERVER_IP>:10010/ 加人/停人/重置密码(口令见 .env ACCOUNT_ADMIN_TOKEN)。
account-server:
build: ./account-service
image: tongxun-account-server:latest
container_name: account-server
restart: always
environment:
- OPENIM_API_URL=http://openim-server:10002
- OPENIM_SECRET=${OPENIM_SECRET}
- ADMIN_TOKEN=${ACCOUNT_ADMIN_TOKEN:-admin123}
# 语音通话进房 token 签发(/api/rtc_token),与 livekit 容器用同一对密钥
- LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
- LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
ports:
- "${ACCOUNT_PORT:-10010}:10010"
volumes:
- ./data/account:/app/data
depends_on:
- openim-server
networks:
- openim