# 内部通讯 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} # 离线推送(极光)。未启用时保持空,OpenIM 走 dummy pusher。 # Master Secret 只从环境变量注入,禁止写进仓库。 - IMENV_OPENIM_PUSH_ENABLE=${OPENIM_PUSH_ENABLE:-} - IMENV_OPENIM_PUSH_JPUSH_APPKEY=${JPUSH_APPKEY:-} - IMENV_OPENIM_PUSH_JPUSH_MASTERSECRET=${JPUSH_MASTER_SECRET:-} - IMENV_OPENIM_PUSH_JPUSH_PUSHURL=${JPUSH_PUSH_URL:-https://api.jpush.cn/v3/push} - IMENV_OPENIM_PUSH_JPUSH_PUSHINTENT=${JPUSH_PUSH_INTENT:-} restart: always depends_on: - mongo - redis - etcd - kafka - minio networks: - openim # LiveKit:语音/视频通话的 SFU,Apache 2.0,自部署。 # 客户端通过 ws://: 连接,凭 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 TCP(UDP 不通时的备用通道) - "${LIVEKIT_RTC_UDP_PORT:-17882}:17882/udp" # RTC 媒体(UDP 复用端口) volumes: - ./config/livekit.yaml:/etc/livekit.yaml networks: - openim # 公司账号服务:自研工号+密码登录(替代 OpenIM 样板注册登录),含极简管理页。 # 员工客户端调 http://:10010/api/login 拿 OpenIM token; # 管理员开 http://: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