Routr 2.25.1 Docker Compose 部署指南

面向用 Docker Compose 托管 SIP 信令服务的运维工程师,基于官方 all-in-one 镜像 fonoster/routr-one:2.25.1 完成 Routr 的容器化部署——覆盖镜像认知、端口与数据卷盘点、compose 编排、启动日志三阶段解读、rctl/SDK 双路径资源初始化、SIP REGISTER 鉴权握手验证、重启与重建两种持久化实测、备份与升级,全部命令与回显均为实测。

RoutrRoutrSIPVoIP2026/10/26

Routr 2.25.1 Docker Compose 部署指南

目标:面向用 Docker Compose 托管 SIP 信令服务的运维工程师,基于官方 all-in-one 镜像 fonoster/routr-one:2.25.1 完成 Routr 的容器化部署——覆盖镜像认知、端口与数据卷盘点、compose 编排、启动日志三阶段解读、rctl/SDK 双路径资源初始化、SIP REGISTER 鉴权握手验证、重启与重建两种持久化实测、备份与升级,全部命令与回显均为实测。

一、实验环境说明

项目取值
宿主系统Linux x86_64(Docker 28+,Docker Compose v2)
Routr 版本2.25.1(2026-10-26 发布,截至写作日最新稳定版)
镜像fonoster/routr-one:2.25.1,digest sha256:eb9eeef293b9...
镜像体积解压后 1.13 GB(docker images 实测;Docker Hub 压缩下载约 360 MB)
实验网段自定义 bridge 网络 routr-net(10.89.57.0/24),容器固定 IP 10.89.57.10
宿主端口映射8128→51908(API)、5060/udp+tcp(SIP)

版本选择建议:生产环境锁小版本 tag(如 2.25.1)而非 latest。官方 Docker 部署文档亦提示"除快速测试外,请将 latest 替换为具体版本"。

二、镜像认知:routr-one 是什么

fonoster/routr-one 是官方 all-in-one 发行形态:把 EdgePort(SIP 边缘)、Message Dispatcher、Connect 处理器、Location Service、API Server 与内置 PostgreSQL 打包进一个容器。官方定位是"开发、演示与单主机部署(如家庭/办公室电话系统)"。

与它的另一条路线是 routr/routr-connect Helm chart(K8s 各组件独立伸缩),两者运行的是同一套服务。本章只讲单主机 Compose 路线。

内置组件与端口盘点(容器内监听,实测自启动日志):

端口传输服务是否需要发布到宿主
5060UDP+TCPEdgePort(SIP 信令)需要话机直连时发布
5061TCPSIP over TLS用到 TLS 时发布
5062/5063TCPSIP over WS/WSS(WebRTC 信令)用到浏览器端时发布
51908TCPAPI Server 外部口(rctl/SDK 走这里)管理端点不在本机时发布
51901/51902/51904/51907/51909/51910TCPdispatcher/location/connect/pgdata 内部口/requester/registry不要发布,仅容器内部 gRPC 互访
5432TCP内置 PostgreSQL(仅监听 127.0.0.1/容器内)不要发布

数据落盘位置:内置 PostgreSQL 数据目录 /var/lib/postgresql/data——这是 routr-one 唯一需要持久化的路径(资源数据存库里;注册信息在内存中,重启即失,见第七节实测)。

三、Compose 编排

3.1 compose.yaml

name: routr-2-25-1-stack
services:
  routr:
    image: fonoster/routr-one:2.25.1
    container_name: routr-2-25-1-allinone
    environment:
      EXTERNAL_ADDRS: 10.89.57.10        # 对外通告地址,必填
      LOCALNETS: 10.89.57.0/24           # 本地网段声明
    ports:
      - "8128:51908"                     # API Server 外部口(rctl/SDK)
      - "5060:5060/udp"                  # SIP UDP
      - "5060:5060"                      # SIP TCP
    networks:
      routr-net:
        ipv4_address: 10.89.57.10        # 固定 IP,与 EXTERNAL_ADDRS 一致
    volumes:
      - routr-pgdata:/var/lib/postgresql/data
networks:
  routr-net:
    name: routr-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.57.0/24
volumes:
  routr-pgdata:

3.2 编排要点逐条说明

  1. EXTERNAL_ADDRS 是必填项。Routr 把它写进 Via/Contact/Record-Route 头,填"话机能访问到的地址"(局域网 IP 或公网 IP)。容器内无法自动探测宿主地址,缺了它注册与呼叫都会出问题——这是官方排障清单第一条。
  2. LOCALNETS 与 EXTERNAL_ADDRS 配套:EdgePort 依据它判断对端是内网还是外网,从而决定通告内网地址还是外部地址。本实验容器 IP 与宿主映射并存,显式声明网段消除歧义。
  3. 固定容器 IP 不是必需项,但让 EXTERNAL_ADDRS 有了稳定取值;若用官方最简编排(只映射端口),EXTERNAL_ADDRS 应填宿主机地址。
  4. 端口按需发布:本实验只发 5060 的 UDP+TCP 与 API 口。TLS/WS/WSS 场景再加 5061/5062/5063。
  5. 数据卷必须挂 /var/lib/postgresql/data。用 named volume 而非 bind mount,避免 alpine 容器与宿主 UID 不一致问题。

启动:

docker compose up -d
[+] Running 3/3
 ✔ Network routr-net              Created
 ✔ Volume "routr-2-25-1-stack_routr-pgdata"  Created
 ✔ Container routr-2-25-1-allinone  Started

状态确认(等 30–60 秒让内置数据库完成初始化):

docker ps --filter name=routr-2-25-1-allinone --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES                   IMAGE                       STATUS         PORTS
routr-2-25-1-allinone   fonoster/routr-one:2.25.1   Up 11 minutes  0.0.0.0:5060->5060/tcp, 0.0.0.0:5060->5060/udp, 0.0.0.0:8128->51908/tcp

宿主侧监听确认(ss 可见 docker-proxy 占位):

LISTEN 0  4096  0.0.0.0:5060  ...  users:(("docker-proxy",...))
LISTEN 0  4096  0.0.0.0:8128  ...  users:(("docker-proxy",...))
UNCONN 0  0     0.0.0.0:5060  ...  users:(("docker-proxy",...))   ← UDP

四、启动日志三阶段解读

首次启动约 60–90 秒,日志分三个阶段(docker logs -f routr-2-25-1-allinone):

阶段一:内置 PostgreSQL 初始化(仅首次)

The files belonging to this database system will be owned by user "postgres".
...
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
creating configuration files ... ok
2026-10-26 11:35:02.024 UTC [17] LOG:  starting PostgreSQL 18.6 on x86_64-alpine-linux-musl ...

内置数据库为 PostgreSQL 18.6(Alpine 构建),首次启动执行 initdb,数据写入挂载卷。

阶段二:Prisma 数据库迁移(每次启动都会跑)

PostgreSQL database routr created at localhost:5432
6 migrations found in prisma/migrations
Applying migration `20230123032410_init`
Applying migration `20240212032900_added_max_contacts`
Applying migration `20240228032947_increased_tel_url_length`
Applying migration `20261023000000_added_trace_enabled`
Applying migration `20261025000000_added_trunk_identity`
Applying migration `20261025100000_renamed_trunk_normalize_to_e164`
All migrations have been successfully applied.

迁移幂等:重启后日志变为 No pending migrations to apply.——这正是官方"升级 = 换镜像 tag 重建容器"做法的基础:新版本容器启动时自动把库结构迁移到位。

阶段三:自签证书生成 + 各服务启动

server.crt or server.key files not found. Generating certificates...
Certificate request self-signature ok
subject=CN=localhost
PKCS12 keystore has been created at /etc/routr/certs/signaling.p12
2026-10-01 11:36:33.241 [info]: (one) routr v2 // all in one distribution {}
2026-10-01 11:36:33.316 [info]: (pgdata) starting routr service {"bindAddr":"0.0.0.0:51907","name":"pgdata"}
2026-10-01 11:36:33.398 [info]: (common) starting routr service {"name":"dispatcher","bindAddr":"0.0.0.0:51901"}
2026-10-01 11:36:33.398 [info]: (common) starting routr service {"name":"connect","bindAddr":"0.0.0.0:51904"}
2026-10-01 11:36:33.399 [info]: (pgdata) external server started {"externalServerBindAddr":"0.0.0.0:51908"}
2026-10-26 11:36:36.257 [info]: (edgeport) GRPCSipListener.java starting edgeport ref = 958e880eb2dc version = 2.25.1 at 0.0.0.0
2026-10-26 11:36:36.260 [info]: (edgeport) GRPCSipListener.java localnets list [10.89.57.0/24]
2026-10-26 11:36:36.260 [info]: (edgeport) GRPCSipListener.java external hosts list [10.89.57.10]

看到 starting edgeport ... version = 2.25.1 且 localnets/external hosts 与编排一致,即启动成功。Location 缓存默认 MEMORY(注册不落盘的根因)。

五、资源初始化:rctl 与 SDK 双路径

5.1 安装 rctl 并连通

rctl 是官方 CLI(npm 包 @routr/ctl,需要 Node.js 环境):

npm install --location=global @routr/ctl
rctl --version
@routr/ctl/2.25.0 linux-x64 node-v24.22.1

实测注记:npm 上当前 CLI 版本 2.25.0,服务端 2.25.1——两者小版本错位不影响资源管理。

连接测试(API 口未开 TLS,必须 --insecure;注意 flags 要放在资源命令之后,放在最前面会被 oclif 误解析为子命令):

rctl domains get -e localhost:8128 -i
# 空列表:无表格输出,退出码 0

5.2 交互式创建(rctl create)

rctl domains create -e localhost:8128 -i

按提示输入 Friendly Name(如 Local domain)、Domain URI(如 sip.local),最后确认 Ready?。create/update 没有字段级参数,全部走交互 prompt——脚本化场景改用 SDK(5.3)。

5.3 SDK 脚本化创建(实测)

npm install @routr/sdk 后,用一段 Node.js 脚本幂等地建齐三类资源(Domain / Credentials / Agent):

const SDK = require("@routr/sdk").default
const ENDPOINT = "localhost:8128"   // rctl/SDK 连的是 API Server 外部口

const domains = new SDK.Domains({ endpoint: ENDPOINT, insecure: true })
const credentials = new SDK.Credentials({ endpoint: ENDPOINT, insecure: true })
const agents = new SDK.Agents({ endpoint: ENDPOINT, insecure: true })

async function main() {
  const domain = await domains.createDomain({ name: "Local domain", domainUri: "sip.local" })
  const cred = await credentials.createCredentials({
    name: "Agent credentials", username: "admin", password: "q1w2e3r4" })
  const agent = await agents.createAgent({
    name: "Agent 1001", username: "1001",
    domainRef: domain.ref, credentialsRef: cred.ref,
    maxContacts: -1, enabled: true })
  console.log(domain.ref, cred.ref, agent.ref)
}
main()

实测输出:

domain ref: 7117e0cd-4459-4af4-931a-d406d65d57e6 sip.local
credentials ref: 2708eae6-d0e9-4128-8cbe-fa8bfcc049c3 admin
agent ref: 7055df5a-5a2e-4be6-b303-2b9ae7005da5 1001

rctl 复核:

rctl domains get -e localhost:8128 -i
rctl credentials get -e localhost:8128 -i
rctl agents get -e localhost:8128 -i -x
Name         URI
Local domain sip.local

Name              Username
Agent credentials admin

Ref                                  Name       Username Domain    Max Contacts Privacy Enabled
7055df5a-5a2e-4be6-b303-2b9ae7005da5 Agent 1001 1001     sip.local              Private Yes

六、SIP 注册验证:401 → Digest → 200 全握手

在 Routr 同网络起一个装了 sipsak 的辅助容器,向 EdgePort 发起带鉴权的 REGISTER(/etc/hosts 里把 sip.local 指到 10.89.57.10):

docker run -d --name sip-uac --network routr-net ubuntu:22.04 sleep infinity
docker exec sip-uac bash -c "apt-get update && apt-get install -y sipsak \
  && echo '10.89.57.10 sip.local' >> /etc/hosts"
docker exec sip-uac sipsak -U -s sip:1001@sip.local -u admin -a q1w2e3r4 -vvv

完整握手实测(节选):

REGISTER sip:sip.local SIP/2.0            ← 第 1 发:无鉴权
To: sip:1001@sip.local
SIP/2.0 401 Unauthorized                  ← Routr 要求 Digest 鉴权
To: <sip:1001@sip.local>
WWW-Authenticate: Digest realm="sip.local",domain="",nonce="4b4b0f7e41d8c833ffa0a40173b51ce2",
  algorithm=MD5,qop="auth",opaque=""

REGISTER sip:sip.local SIP/2.0            ← 第 2 发:带 Authorization
Authorization: Digest username="admin", uri="sip:sip.local", algorithm=MD5,
  realm="sip.local", nonce="4b4b0f7e41d8c833ffa0a40173b51ce2", qop=auth,
  nc=00000001, cnonce="55b4d501", response="d7d2ee9b2d219e47c8c488037b5f07c3"
SIP/2.0 200                               ← 鉴权通过,注册成功
To: <sip:1001@sip.local>

All usrloc tests completed successful.

要点:realm 恰为 Domain URI(sip.local),鉴权账号是 Credentials 资源里的 admin/q1w2e3r4,AOR 中的用户 1001 是 Agent 的 username——三个资源在一次握手里全部得到验证。

话机配置对照:SIP 服务器/域 = 10.89.57.10(或解析到该地址的域名),用户名 = 1001,鉴权用户 = admin,密码 = q1w2e3r4。注意"用户名"与"鉴权用户名"是两个字段——这是 Connect 模式最容易配错的地方。

七、持久化实测:重启保留什么、丢失什么

7.1 restart:资源保留(存 pgdata 卷)

docker compose restart && sleep 12
rctl agents get -e localhost:8128 -i
Name       Username Domain    Max Contacts Privacy Enabled
Agent 1001 1001     sip.local              Private Yes

重启后 Domain/Credentials/Agent 全部还在——资源数据在内置 PostgreSQL 里,落在 routr-pgdata 卷上。

7.2 down + up:容器可全换,数据卷不删就不丢

docker compose down      # 容器与网络删除,卷保留
docker volume ls --filter name=routr-2-25-1-stack
# → routr-2-25-1-stack_routr-pgdata   (卷仍在)
docker compose up -d && sleep 45
rctl agents get -e localhost:8128 -i    # Agent 1001 依旧在

丢失的是注册信息:Location 缓存为 MEMORY,重启即清空,话机需重新注册。官方文档同样明示这一点。生产建议:话机注册有效期(Expires)不要设得过长,并接受"容器重启 = 全量重注册"的运维现实;要跨实例共享注册需改用 Redis(外部 PostgreSQL + START_INTERNAL_DB=false,Location 换 redis provider,参考第八节环境变量)。

7.3 备份:直接 pg_dump 内置库

容器内自带 postgres 客户端工具,备份不必停服:

docker exec routr-2-25-1-allinone pg_dump -U postgres -d routr > routr-backup.sql

实测库表清单(Prisma 模型):

 public | _prisma_migrations   | table | postgres
 public | access_control_lists | table | postgres
 public | agents               | table | postgres
 public | credentials          | table | postgres
 public | domains              | table | postgres
 public | egress_policies      | table | postgres
 public | numbers              | table | postgres
 public | peers                | table | postgres
 public | trunk_uris           | table | postgres
 public | trunks               | table | postgres

八、常用环境变量(routr-one)

变量默认说明
EXTERNAL_ADDRS无必填。对外通告地址,逗号分隔可多个
LOCALNETS系统探测本地网段(逗号分隔 CIDR)
TLS_ONfalseAPI Server 51908 启用 TLS(开启后 rctl 不再要 --insecure,改传 --cacert)
VERIFY_CLIENT_CERTfalseAPI Server 强制客户端证书
DATABASE_URL内置库外部 PostgreSQL 连接串
START_INTERNAL_DBtrue指向外部库时置 false 关内置 PG
RTPENGINE_HOST / RTPENGINE_PORT无 / 22222启用 RTPRelay 中间件(WebRTC 媒体桥)
PKCS12_PASSWORDchangemeSIP 信令 keystore 密码,生产必须改
HEPLIFY_OPTIONS无向 Homer 抓包分析器镜像 SIP 流量

九、升级与排障

9.1 升级流程

# 1. 备份(见 7.3),并阅读目标版本 changelog
# 2. 修改 compose.yaml 中的 image tag
# 3. 拉取并重建
docker compose pull
docker compose up -d

原理:容器每次启动自动执行 pending 迁移(第四章阶段二),老卷 + 新镜像 = 原地升级。回退 = 改回旧 tag 重建。

9.2 高频故障速查(含官方排障清单)

现象根因与处置
注册/呼叫单通或回包丢失EXTERNAL_ADDRS 缺失或填错(填成了容器内网地址)。改为话机可达地址后重建
rctl 报 TLS/连接错误API 口默认无 TLS,须加 -i/--insecure;TLS_ON=true 时改用 --cacert 指向 CA
rctl 提示 command -e not found全局 flags 写在了资源命令前面。移到动作之后:rctl domains get -e ... -i
UDP 通、TCP 不通只发布了 5060/udp。按需补 - "5060:5060"(TCP)及 5061–5063
down 后数据消失数据卷没挂 /var/lib/postgresql/data,或误用 down -v(会删卷,生产禁用)
重启后话机全部掉线预期行为:注册在内存。话机自动重注册即可
起动 60 秒内 rctl 连不上内置库首启 + 迁移未完成,等日志出现 starting edgeport 再操作

十、清理

docker compose down              # 保留数据卷
docker compose down -v           # 连数据卷一起删(确认无用后再执行)
docker rmi fonoster/routr-one:2.25.1
docker volume rm routr-2-25-1-stack_routr-pgdata

十一、参考