Yeti-Switch Docker Compose 部署指南
Yeti-Switch 官方无 Docker Hub 镜像,本文基于 Debian 13 trixie 官方 deb 包逐层构建 PostgreSQL 18/yeti 扩展、SEMS yeti 流量平面、yeti-web 管理平面三个自定义镜像,覆盖 host 网络 + SIP/RTP UDP 端口映射。
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:18 | docker.io | 官方 PostgreSQL 18,需在容器内额外安装 pgq / yeti 扩展包(来自 trixie-pgdg) |
redis:7 | docker.io | 官方 Redis 7,启动参数需加 --notify-keyspace-events "Egx" |
debian:13 | docker.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/data | PostgreSQL 18 路由库 |
| cdr-db | ${VOLUME_CDR} → /var/lib/postgresql/data | PostgreSQL 18 CDR 库(容量通常 ≥ routing 5 倍) |
| Redis | ${VOLUME_REDIS} → /data | 容量/注册/计数器 |
| yeti-web | ${VOLUME_WEB} → /opt/yeti-web/tmp | Rails 临时文件(upload / puma socket) |
| SEMS | ${VOLUME_SEMS} → /var/spool/sems | Pcap 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 | ✅ | yeti | routing-db 数据库名 |
POSTGRES_USER | ✅ | yeti | routing-db 用户 |
POSTGRES_PASSWORD | ✅ | — | routing-db 密码(生产务必改) |
CDR_DB | ✅ | cdr | cdr-db 数据库名 |
CDR_USER | ✅ | cdr | cdr-db 用户 |
CDR_PASSWORD | ✅ | — | cdr-db 密码 |
PG_VERSION | ✅ | 18 | PostgreSQL 主版本(yeti 扩展绑定 18) |
YETI_POP_ID | ✅ | 1 | PoP ID,yet-web 创建后填入 |
YETI_NODE_ID | ✅ | 1 | Node ID |
WEB_PORT | ✅ | 80 | Nginx 对外端口 |
VOLUME_ROUTING | ✅ | yeti-routing-data | routing-db 卷名 |
VOLUME_CDR | ✅ | yeti-cdr-data | cdr-db 卷名 |
VOLUME_REDIS | ✅ | yeti-redis-data | Redis 卷名 |
VOLUME_WEB | ✅ | yeti-web-data | yeti-web 卷名 |
VOLUME_SEMS | ✅ | yeti-sems-data | SEMS 卷名 |
七、等价 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 容量计数器跨节点同步。
十、参考链接
- 官方 1.14 安装指南 https://yeti-switch.org/docs/installation/installation-1.14/
- SEMS 安装 https://yeti-switch.org/docs/installation/installation-1.14/sems.html
- yeti-web 安装 https://yeti-switch.org/docs/installation/installation-1.14/web.html
- Redis 配置 https://yeti-switch.org/docs/installation/installation-1.14/redis.html
- PostgreSQL 18 PGDG https://wiki.postgresql.org/wiki/Apt
- postgres:18 Docker Hub https://hub.docker.com/_/postgres
- redis:7 Docker Hub https://hub.docker.com/_/redis
- Docker Compose v2 文档 https://docs.docker.com/compose/
- Changelog https://yeti-switch.org/docs/changelog.html