Yeti-Switch Docker Compose 部署指南

Yeti-Switch 官方无 Docker Hub 镜像,本文基于 Debian 13 trixie 官方 deb 包逐层构建 PostgreSQL 18/yeti 扩展、SEMS yeti 流量平面、yeti-web 管理平面三个自定义镜像,覆盖 host 网络 + SIP/RTP UDP 端口映射。

Yeti-SwitchYeti-SwitchSIP 协议VoIP 语音SBC电信软交换REST2026/10/27

Yeti-Switch Docker Compose 部署指南

目标:给出 Yeti-Switch v1.14 基于 Debian 13 (trixie) 官方 .deb 包的 Docker Compose 部署参考方案。重要前提:Yeti-Switch 官方无 Docker Hub 预构建镜像,本文 Dockerfile 基于 Debian 13 + 官方 apt 仓库逐层构建,功能等价于裸机安装,但不建议用于生产(SEMS 媒体平面在容器中的 UDP 端口、host 网络与 sysctls 调优存在额外风险)。检索日期 2026-10-27。

一、环境信息

  • Docker Engine ≥ 24.0、Docker Compose v2(docker compose version 检查)
  • 宿主机 OS:推荐 Debian 13 / Ubuntu 24.04(SEMS 媒体 UDP 需要较新的内核)
  • 公网 IP:SIP/RTP 端口必须从容器中正确对外暴露
  • Yeti-Switch 版本:1.14.x(对应 Debian 13 trixie 官方包)
  • 最低资源:4 vCPU / 8 GB RAM / 50 GB NVMe(容器叠加后资源开销高于裸机)

⚠️ 实验冻结令铁律:Yeti-Switch 安装依赖 SEMS + PostgreSQL 双库 + Redis + Rails,禁止启动复杂搭建实验。本指南不执行 docker build/compose up 验证,所有 Dockerfile 与 Compose 内容来自官方 Debian 13 安装指南的等价映射。

二、镜像选型说明

镜像来源说明
postgres:18docker.io官方 PostgreSQL 18,需在容器内额外安装 pgq / yeti 扩展包(来自 trixie-pgdg)
redis:7docker.io官方 Redis 7,启动参数需加 --notify-keyspace-events "Egx"
debian:13docker.io官方 Debian 13 trixie,用于构建 yeti-sems 与 yeti-web 两个自定义镜像

为什么没有 yeti-switch 官方镜像?

2026-10-27 实查 Docker Hub(docker search yeti-switch、访问 yeti-switch 组织页面)、ghcr.io、quay.io,官方 yeti-switch 未发布任何 Docker Hub 账户或预构建镜像。changelog 提到的 "containers" 是内部 build 期镜像(用于 SEMS/DEB 包打包流水线),不对终端用户开放。本指南的 Dockerfile 完全基于官方 1.14 安装指南的 apt install 步骤在容器内重放。

三、前置条件

# Docker Compose v2 检查
docker compose version
# Docker Compose version v2.29.x

# 网络规划(避开实验占用的 10.89.56–75 段,10.89.60 空闲)
docker network create --subnet 10.89.60.0/24 yeti-net 2>/dev/null

# SIP/RTP 端口开放
ufw allow 5060/udp && ufw allow 5060/tcp && ufw allow 5061/tcp
ufw allow 16384:32769/udp

四、部署步骤

4.1 目录结构

/opt/data/quzuo/tmp/yeti-switch/
├── compose.yaml
├── .env.example
├── postgres-routing.Dockerfile
├── postgres-cdr.Dockerfile
├── sems.Dockerfile
├── yeti-web.Dockerfile
├── nginx-yeti.conf
├── sems.conf
├── yeti_web.yml
└── entrypoints/
    ├── postgres-routing-entrypoint.sh
    ├── postgres-cdr-entrypoint.sh
    └── wait-for-it.sh

4.2 环境变量 .env

# 复制模板
cp .env.example .env
# 修改以下值

.env.example 内容:

# === PostgreSQL ===
POSTGRES_DB=yeti
POSTGRES_USER=yeti
POSTGRES_PASSWORD=q1w2e3r4
CDR_DB=cdr
CDR_USER=cdr
CDR_PASSWORD=q1w2e3r4
PG_VERSION=18

# === Redis ===
REDIS_PORT=6379

# === SEMS / Yeti-Switch ===
# 必须在 yeti-web 初始化完成后创建 PoP / Node 并填写
YETI_POP_ID=1
YETI_NODE_ID=1
SEMS_SIGNALLING_IP=0.0.0.0
SEMS_MEDIA_IP=0.0.0.0

# === Nginx / Web ===
WEB_PORT=80
SEMS_RPC_PORT=7080

# === 数据卷 ===
VOLUME_ROUTING=yeti-routing-data
VOLUME_CDR=yeti-cdr-data
VOLUME_REDIS=yeti-redis-data
VOLUME_WEB=yeti-web-data
VOLUME_SEMS=yeti-sems-data

4.3 PostgreSQL 18 扩展安装 postgres-yeti.Dockerfile

yeti 扩展(pgq / pgq3 / prefix / pllua / yeti)官方只在 trixie-pgdg 源提供,需继承 postgres:18 并运行 apt:

FROM postgres:18-bookworm
# ⚠️ postgres:18 默认基于 Debian bookworm,需切换为 trixie 源
# 官方 1.14 要求 Debian 13 (trixie)

# 用 Debian 13 官方源覆盖
RUN sed -i 's|bookworm|trixie|g' /etc/apt/sources.list.d/*.sources /etc/apt/sources.list 2>/dev/null; \
    cat > /etc/apt/sources.list.d/yeti.list <<'EOF'
deb https://deb.yeti-switch.org/debian/1.14 trixie main
EOF; \
    wget -O /etc/apt/trusted.gpg.d/yeti.asc https://deb.yeti-switch.org/yeti.gpg; \
    wget -O /etc/apt/trusted.gpg.d/pgdg.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc; \
    apt-get update && apt-get install -y \
        postgresql-18-prefix \
        postgresql-18-pgq3 \
        postgresql-18-pgq-ext \
        postgresql-18-yeti \
        postgresql-18-pllua \
        pgqd && \
    rm -rf /var/lib/apt/lists/*

# CDR 库需要初始化 pgq,routing 库不需要

4.4 SEMS sems.Dockerfile

FROM debian:13-slim AS sems-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    wget ca-certificates && \
    wget -O /etc/apt/trusted.gpg.d/yeti.asc https://deb.yeti-switch.org/yeti.gpg && \
    cat > /etc/apt/sources.list.d/yeti.list <<'EOF'
deb https://deb.yeti-switch.org/debian/1.14 trixie main
EOF
RUN apt-get update && apt-get install -y --no-install-recommends \
    sems sems-modules-yeti && \
    rm -rf /var/lib/apt/lists/*

# 复制宿主机 sems.conf(含 pop_id / node_id / db 连接 / redis)
COPY sems.conf /etc/sems/sems.conf
COPY entrypoints/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh

# 媒体目录
RUN mkdir -p /var/spool/sems/dump /var/spool/sems/records

EXPOSE 5060/udp 5060/tcp 5061/tcp 7080/tcp 16384-32769/udp
CMD ["wait-for-it.sh", "postgres-routing:5432", "--", \
     "wait-for-it.sh", "redis:6379", "--", \
     "/usr/sbin/sems", "-f", "/etc/sems/sems.conf", "-F"]

4.5 yeti-web yeti-web.Dockerfile

FROM debian:13-slim AS yeti-web
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    wget ca-certificates gnupg && \
    wget -O /etc/apt/trusted.gpg.d/yeti.asc https://deb.yeti-switch.org/yeti.gpg && \
    wget -O /etc/apt/trusted.gpg.d/pgdg.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc && \
    cat > /etc/apt/sources.list.d/yeti.list <<'EOF'
deb https://deb.yeti-switch.org/debian/1.14 trixie main
EOF
RUN apt-get update && apt-get install -y --no-install-recommends \
    yeti-web nginx sudo && \
    rm -rf /var/lib/apt/lists/*

# database.yml / secrets.yml 由 compose bind mount 提供
# yeti_web.yml 默认模板已在 /opt/yeti-web/config/yeti_web.yml.distr

COPY entrypoints/wait-for-it.sh /usr/local/bin/wait-for-it.sh
RUN chmod +x /usr/local/bin/wait-for-it.sh

EXPOSE 80

4.6 compose.yaml 主编排

name: yeti-switch-114-stack

services:
  # ============== 双 PostgreSQL 18 实例 ==============
  postgres-routing:
    build:
      context: .
      dockerfile: postgres-yeti.Dockerfile
    container_name: yeti-routing-db
    image: yeti/postgres18-yeti:1.14
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ${VOLUME_ROUTING}:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [yeti-net]
    # SEMS 需要 postgres:18-yeti 扩展(routing 库 schema 依赖)

  postgres-cdr:
    extends: postgres-routing
    container_name: yeti-cdr-db
    environment:
      POSTGRES_DB: ${CDR_DB}
      POSTGRES_USER: ${CDR_USER}
      POSTGRES_PASSWORD: ${CDR_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ${VOLUME_CDR}:/var/lib/postgresql/data
    networks: [yeti-net]

  # ============== Redis ==============
  redis:
    image: redis:7
    container_name: yeti-redis
    command: ["redis-server", "--notify-keyspace-events", "Egx"]
    volumes:
      - ${VOLUME_REDIS}:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks: [yeti-net]

  # ============== SEMS yeti(流量平面) ==============
  sems:
    build:
      context: .
      dockerfile: sems.Dockerfile
    container_name: yeti-sems
    image: yeti/sems-yeti:1.14
    depends_on:
      postgres-routing: { condition: service_healthy }
      postgres-cdr:     { condition: service_healthy }
      redis:            { condition: service_healthy }
    network_mode: host  # ⚠️ SIP/RTP 媒体平面强烈建议 host 网络
    # host 网络下无需 ports 映射,直接使用宿主机端口
    environment:
      - SEMS_SIGNALLING_IP=${SEMS_SIGNALLING_IP}
      - SEMS_MEDIA_IP=${SEMS_MEDIA_IP}
    volumes:
      - ./sems.conf:/etc/sems/sems.conf:ro
      - ${VOLUME_SEMS}:/var/spool/sems
    restart: unless-stopped
    # host 网络下 sysctl 需在宿主机设置:
    # sysctl -w net.ipv4.ip_local_port_range="1024 65535"
    # sysctl -w net.core.rmem_max=16777216

  # ============== yeti-web + Nginx(管理平面) ==============
  yeti-web:
    build:
      context: .
      dockerfile: yeti-web.Dockerfile
    container_name: yeti-web
    image: yeti/web:1.14
    depends_on:
      postgres-routing: { condition: service_healthy }
      postgres-cdr:     { condition: service_healthy }
      redis:            { condition: service_healthy }
    ports:
      - "${WEB_PORT}:80"
    environment:
      # database.yml 里用 host: postgres-routing / postgres-cdr
      RAILS_ENV: production
      DB_HOST_PRIMARY: postgres-routing
      DB_HOST_CDR: postgres-cdr
      DB_USER_PRIMARY: ${POSTGRES_USER}
      DB_USER_CDR: ${CDR_USER}
      DB_PASS_PRIMARY: ${POSTGRES_PASSWORD}
      DB_PASS_CDR: ${CDR_PASSWORD}
    volumes:
      # 关键配置文件 bind mount(首次运行前初始化)
      - ./web-config/database.yml:/opt/yeti-web/config/database.yml
      - ./web-config/secrets.yml:/opt/yeti-web/config/secrets.yml
      - ./web-config/yeti_web.yml:/opt/yeti-web/config/yeti_web.yml
      - ./nginx-yeti.conf:/etc/nginx/sites-available/yeti
      - ${VOLUME_WEB}:/opt/yeti-web/tmp
    networks: [yeti-net]
    restart: unless-stopped
    # 首次启动时执行 db:schema:load + db:migrate + db:seed

⚠️ 注意:SIP/RTP 媒体平面的容器网络问题。推荐用 network_mode: host 让 SEMS 直接使用宿主机网络栈,否则 UDP NAT 可能导致 RTP 流不通。如果必须用 bridge 网络,需要显式 ports: ["5060:5060/udp", "16384-32769:16384-32769/udp"] 且在 sems.conf 中手动设置 external-ip / symmetric-rtp-mode。

4.7 SEMS 配置 sems.conf(容器版)

与裸机版本的关键差异:数据库 host 用 compose service 名 postgres-routing / postgres-cdr,redis host 用 redis。

general {
  node_id = 1
  # ... 其余与 B1 §4.6 相同 ...
}
signaling-interfaces {
  interface primary {
    ip4 {
      sip-udp { address = 0.0.0.0 port = 5060 use-raw-sockets = off }
      sip-tcp { address = 0.0.0.0 port = 5060 idle-timeout = 900000 use-raw-sockets = off }
    }
  }
}
media-interfaces {
  interface primary {
    ip4 { rtp { address = 0.0.0.0 low-port = 16384 high-port = 32769 dscp = 46 } }
  }
}
modules {
  # ... 省略与 B1 相同的常规模块 ...
  module "jsonrpc" { listen { address = 127.0.0.1 port = 7080 } server_threads = 1 }
  module "redis" { max_batch_size = 10 batch_timeout = 100 }
  module "registrar" {
    redis { write { hosts = { redis:6379 } } read { hosts = { redis:6379 } } }
  }
  module "yeti" {
    pop_id = 1
    msg_logger_dir = /var/spool/sems/dump
    audio_recorder_dir = /var/spool/sems/records
    db_refresh_interval = 5
    routing {
      master_pool { host = postgres-routing port = 5432 name = yeti user = yeti pass = q1w2e3r4 size = 4 }
    }
    cdr {
      master { host = postgres-cdr port = 5432 name = cdr user = cdr pass = q1w2e3r4 }
    }
    resources {
      write { hosts = { redis:6379 } timeout = 500 }
      read  { hosts = { redis:6379 } timeout = 1000 }
    }
    rpc { calls_show_limit = 10000 }
  }
}
routing { application = yeti }

4.8 yeti-web database.yml(容器版)

production:
  primary:
    adapter: postgresql
    encoding: unicode
    database: yeti
    username: yeti
    password: q1w2e3r4
    host: postgres-routing
    port: 5432
    schema_search_path: 'gui, public, switch, billing, class4, runtime_stats, sys, logs, data_import'
    min_messages: notice
    pool: 5
  cdr:
    adapter: postgresql
    encoding: unicode
    database: cdr
    username: cdr
    password: q1w2e3r4
    host: postgres-cdr
    port: 5432
    schema_search_path: 'cdr, reports, billing'
    min_messages: notice
    pool: 5
    migrations_paths: db/cdr_migrate

4.9 首次启动流程

# 1. 生成 secrets.yml(需先安装 pwgen:apt install pwgen)
mkdir -p web-config
echo "production:" > web-config/secrets.yml
echo "  secret_key_base: $(pwgen -s 128 1)" >> web-config/secrets.yml

# 2. 构建镜像(SEMS + yeti-web + postgres-yeti)
docker compose build

# 3. 启动数据库与 Redis
docker compose up -d postgres-routing postgres-cdr redis
docker compose ps
# 三个容器 healthy

# 4. 初始化 yeti-web 数据库 schema + seed
docker compose run --rm yeti-web bash -c \
  "cd /opt/yeti-web && \
   cp config/yeti_web.yml.distr config/yeti_web.yml 2>/dev/null; \
   su -s /bin/bash yeti-web -c \
     'RAILS_ENV=production ./bin/bundle.sh exec rake db:schema:load db:migrate db:seed && \
      RAILS_ENV=production ./bin/bundle.sh exec rake custom_seeds[network_prefixes]'"

# 5. 启动 yeti-web 与 SEMS
docker compose up -d
docker compose ps

# 6. 首次登录 http://<宿主机 IP>/ admin / 111111
#    System → Points of Presence → 创建 PoP → 记下 id
#    System → Nodes → 创建 Node,填宿主机 RPC endpoint(host 网络下 127.0.0.1:7080)
#    把 pop_id 写回 sems.conf 的 yeti module.pop_id 行
#    重新构建 sems 容器

五、数据持久化说明

数据挂载路径说明
routing-db${VOLUME_ROUTING} → /var/lib/postgresql/dataPostgreSQL 18 路由库
cdr-db${VOLUME_CDR} → /var/lib/postgresql/dataPostgreSQL 18 CDR 库(容量通常 ≥ routing 5 倍)
Redis${VOLUME_REDIS} → /data容量/注册/计数器
yeti-web${VOLUME_WEB} → /opt/yeti-web/tmpRails 临时文件(upload / puma socket)
SEMS${VOLUME_SEMS} → /var/spool/semsPcap dump + 录音

宿主机路径绑定(可选,替代 named volumes):

volumes:
  - /opt/data/quzuo/tmp/yeti-switch/routing-data:/var/lib/postgresql/data
  - /opt/data/quzuo/tmp/yeti-switch/cdr-data:/var/lib/postgresql/data
  - /opt/data/quzuo/tmp/yeti-switch/redis-data:/data

六、环境变量表

变量必填默认说明
POSTGRES_DB✅yetirouting-db 数据库名
POSTGRES_USER✅yetirouting-db 用户
POSTGRES_PASSWORD✅—routing-db 密码(生产务必改)
CDR_DB✅cdrcdr-db 数据库名
CDR_USER✅cdrcdr-db 用户
CDR_PASSWORD✅—cdr-db 密码
PG_VERSION✅18PostgreSQL 主版本(yeti 扩展绑定 18)
YETI_POP_ID✅1PoP ID,yet-web 创建后填入
YETI_NODE_ID✅1Node ID
WEB_PORT✅80Nginx 对外端口
VOLUME_ROUTING✅yeti-routing-datarouting-db 卷名
VOLUME_CDR✅yeti-cdr-datacdr-db 卷名
VOLUME_REDIS✅yeti-redis-dataRedis 卷名
VOLUME_WEB✅yeti-web-datayeti-web 卷名
VOLUME_SEMS✅yeti-sems-dataSEMS 卷名

七、等价 docker run 写法

# 一次性启动双库 + Redis + SEMS + yeti-web(host 网络,简化版)

docker run -d --name yeti-routing --network yeti-net \
  -e POSTGRES_USER=yeti -e POSTGRES_PASSWORD=q1w2e3r4 \
  -e POSTGRES_DB=yeti \
  -v yeti-routing-data:/var/lib/postgresql/data \
  yeti/postgres18-yeti:1.14

docker run -d --name yeti-cdr --network yeti-net \
  -e POSTGRES_USER=cdr -e POSTGRES_PASSWORD=q1w2e3r4 \
  -e POSTGRES_DB=cdr \
  -v yeti-cdr-data:/var/lib/postgresql/data \
  yeti/postgres18-yeti:1.14

docker run -d --name yeti-redis --network yeti-net \
  -v yeti-redis-data:/data \
  redis:7 redis-server --notify-keyspace-events "Egx"

# SEMS(host 网络,SIP/RTP 媒体平面推荐)
docker run -d --name yeti-sems --network host \
  -v /opt/data/quzuo/tmp/yeti-switch/sems.conf:/etc/sems/sems.conf:ro \
  -v yeti-sems-data:/var/spool/sems \
  yeti/sems-yeti:1.14

# yeti-web(bridge 网络,管理平面)
docker run -d --name yeti-web --network yeti-net \
  -p 80:80 \
  -v /opt/data/quzuo/tmp/yeti-switch/web-config/database.yml:/opt/yeti-web/config/database.yml \
  -v /opt/data/quzuo/tmp/yeti-switch/web-config/secrets.yml:/opt/yeti-web/config/secrets.yml \
  -v /opt/data/quzuo/tmp/yeti-switch/web-config/yeti_web.yml:/opt/yeti-web/config/yeti_web.yml \
  yeti/web:1.14

八、常用运维命令

# 查看全部容器状态
docker compose ps

# 查看 SEMS 实时日志
docker compose logs -f sems

# 查看 yeti-web Rails 日志
docker compose logs -f yeti-web

# 重建 yeti-web(换配置后)
docker compose up -d --build yeti-web

# 重建 SEMS(换 sems.conf 后)
docker compose up -d --build sems

# 进入数据库检查
docker compose exec postgres-routing psql -U yeti -d yeti -c "\dt"
docker compose exec postgres-cdr     psql -U cdr  -d cdr  -c "\dt"

# 停止并移除全部容器 + 网络(保留数据卷)
docker compose down

# 彻底清理(含数据卷)
docker compose down -v
docker image rm yeti/postgres18-yeti:1.14 yeti/sems-yeti:1.14 yeti/web:1.14
docker network rm yeti-net

九、常见问题

Q1:SEMS 容器启动但 systemctl status sems 不存在?
SEMS 在容器中直接作为 PID 1 运行(/usr/sbin/sems -F),不经过 systemd。用 docker compose top sems 查看进程,docker compose logs sems 看日志。

Q2:RTP 媒体不通,对端一直 488 Not Acceptable Here?
SEMS 默认 network_mode: host 用宿主机网络,检查宿主机防火墙是否放通 16384–32769 UDP 范围。bridge 网络下必须显式 ports 映射且设置 symmetric-rtp-mode。

Q3:postgres:18 容器启动后 yeti 扩展报 could not find shared library?
postgres:18 默认基于 Debian bookworm,而 trixie-pgdg 源的 yeti 扩展是为 Debian 13 编译的。必须继承 Debian 13 slim 或在 Dockerfile 里切换到 trixie 源再装 postgresql-18-yeti。

Q4:SEMS JSONRPC 暴露到容器外有安全风险?
host 网络下 SEMS 直接绑定 127.0.0.1:7080 到宿主机。ufw 需禁 7080 对外,仅 yeti-web(同宿主机)可访问。

Q5:生产推荐容器部署还是裸机?
裸机。SEMS 媒体平面在容器中的性能、UDP NAT、sysctls 调优、网络 namespace 隔离都比裸机复杂。本文 Compose 方案适用于 PoC / 开发环境。

Q6:多节点集群 Compose 写法?
每个 Switch Node 单独一个 compose 栈(同一 Redis + 双库),在 yeti-web System → Nodes 注册多个 Node,Redis 容量计数器跨节点同步。

十、参考链接