Routr 2.25.1 Docker Compose 部署指南
面向用 Docker Compose 托管 SIP 信令服务的运维工程师,基于官方 all-in-one 镜像 fonoster/routr-one:2.25.1 完成 Routr 的容器化部署——覆盖镜像认知、端口与数据卷盘点、compose 编排、启动日志三阶段解读、rctl/SDK 双路径资源初始化、SIP REGISTER 鉴权握手验证、重启与重建两种持久化实测、备份与升级,全部命令与回显均为实测。
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 路线。
内置组件与端口盘点(容器内监听,实测自启动日志):
| 端口 | 传输 | 服务 | 是否需要发布到宿主 |
|---|---|---|---|
| 5060 | UDP+TCP | EdgePort(SIP 信令) | 需要话机直连时发布 |
| 5061 | TCP | SIP over TLS | 用到 TLS 时发布 |
| 5062/5063 | TCP | SIP over WS/WSS(WebRTC 信令) | 用到浏览器端时发布 |
| 51908 | TCP | API Server 外部口(rctl/SDK 走这里) | 管理端点不在本机时发布 |
| 51901/51902/51904/51907/51909/51910 | TCP | dispatcher/location/connect/pgdata 内部口/requester/registry | 不要发布,仅容器内部 gRPC 互访 |
| 5432 | TCP | 内置 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 编排要点逐条说明
EXTERNAL_ADDRS是必填项。Routr 把它写进Via/Contact/Record-Route头,填"话机能访问到的地址"(局域网 IP 或公网 IP)。容器内无法自动探测宿主地址,缺了它注册与呼叫都会出问题——这是官方排障清单第一条。LOCALNETS与EXTERNAL_ADDRS配套:EdgePort 依据它判断对端是内网还是外网,从而决定通告内网地址还是外部地址。本实验容器 IP 与宿主映射并存,显式声明网段消除歧义。- 固定容器 IP 不是必需项,但让
EXTERNAL_ADDRS有了稳定取值;若用官方最简编排(只映射端口),EXTERNAL_ADDRS应填宿主机地址。 - 端口按需发布:本实验只发 5060 的 UDP+TCP 与 API 口。TLS/WS/WSS 场景再加 5061/5062/5063。
- 数据卷必须挂
/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_ON | false | API Server 51908 启用 TLS(开启后 rctl 不再要 --insecure,改传 --cacert) |
VERIFY_CLIENT_CERT | false | API Server 强制客户端证书 |
DATABASE_URL | 内置库 | 外部 PostgreSQL 连接串 |
START_INTERNAL_DB | true | 指向外部库时置 false 关内置 PG |
RTPENGINE_HOST / RTPENGINE_PORT | 无 / 22222 | 启用 RTPRelay 中间件(WebRTC 媒体桥) |
PKCS12_PASSWORD | changeme | SIP 信令 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
十一、参考
- 官方 Docker 部署文档:https://docs.routr.io/guides/deploy-with-docker
- 部署拓扑对比:https://docs.routr.io/concepts/deployment-topologies
- 镜像 tags:https://hub.docker.com/r/fonoster/routr-one/tags
- rctl 参考:https://docs.routr.io/reference/rctl
- SDK 参考:https://docs.routr.io/reference/sdk
- 环境变量总表:https://docs.routr.io/reference/environment-variables
- 本专栏姊妹篇:《Routr 2.25.1 介绍与选型指南》(认知与选型)、《Routr 2.25.1 配置参数详解》(配置文件逐项解析)、《Routr 2.25.1 常用 CLI 命令与工具详解》(rctl 全命令实测)、《Ubuntu 22.04 安装 Routr 2.25.1 指南》(无 Docker 裸机路线)