ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务

LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的数据库,结果就是迁移一次环境要折腾大半天,换台机器就得重新踩一遍依赖坑。容器化方案把运行环境连同依赖一起打包,镜像拉下来就能跑,这对需要频繁搭建测试环境、或者要在边缘侧快速交付的团队来说,省下的时间非常可观。

ChirpStack 是一套开源的 LoRaWAN 网络服务器组件集合,包含网络服务器、应用服务器、网关桥接以及 Web 管理界面等模块。它支持标准的 LoRaWAN 协议,能对接市面上大多数网关设备,社区活跃度也高。把它跑在容器里,最大的好处是各组件之间的依赖关系被隔离清楚,数据库、消息队列、缓存各占一个容器,出问题的时候排查范围明确,不会出现“改了一个配置结果把另一个服务搞挂”的连锁反应。

这篇文章面向的是刚接触 LoRa 网络搭建的开发者、需要快速验证方案的物联网工程师,以及想在自己实验室里复现一套完整 LoRaWAN 链路的同学。我会从整体架构讲起,把每个容器的职责、配置文件的对应关系、端口映射的逻辑都拆开说清楚,然后给出可以直接照着敲的完整操作流程。中间会穿插我在实际搭建过程中踩过的坑,比如数据库初始化顺序、网关桥接的配置陷阱、区域参数选错导致设备入网失败等问题。看完之后你应该能独立搭起一套可用的环境,并且知道每个环节出问题时该往哪个方向查。

需要提前说明的是,LoRaWAN 涉及区域参数、频段规划等内容,不同地区的法规要求不同,实际操作时请依据当地适用的技术规范来配置。本文重点放在技术实现层面,不涉及具体地区的政策讨论。

2. 整体架构与组件职责拆解

2.1 核心组件一览与数据流向

一套完整的 ChirpStack 容器化部署通常包含以下几个服务单元。理解它们各自的职责,是后续配置和排障的基础。

组件名称容器角色核心职责默认端口
PostgreSQL关系型数据库存储设备信息、网关记录、租户与应用配置5432
Redis内存缓存缓存会话状态、设备激活信息、去重窗口数据6379
MosquittoMQTT 消息代理承载网关与服务器之间、服务器与应用之间的消息1883
ChirpStack Network Server网络服务器处理 LoRaWAN MAC 层逻辑、去重、ADR、入网8000
ChirpStack Application Server应用服务器设备管理、数据解码、集成对接8080
ChirpStack Gateway Bridge网关桥接把网关的原始协议转换为 MQTT 消息1700
ChirpStack Web UI管理界面可视化操作设备、网关、应用8080

数据流向大致是这样的:终端设备发出 LoRa 信号,网关接收后通过 UDP 包转发给网关桥接服务,桥接服务把数据封装成 MQTT 消息发布到 Mosquitto,网络服务器订阅这些消息完成 MAC 层处理,再把解密后的应用数据通过 MQTT 转发给应用服务器,应用服务器最终把数据推送到你配置的 HTTP 接口或者再次通过 MQTT 分发出去。整个链路里,PostgreSQL 负责持久化,Redis 负责加速状态查询。

2.2 为什么用 Docker Compose 而不是单容器

有人可能会想,把所有这些服务塞进一个容器不就行了?理论上可以,但实践中问题很多。首先,每个服务的启动顺序有依赖关系,数据库没起来的时候网络服务器连不上就会反复重启;其次,日志混在一起,排查问题时根本分不清哪条是哪个服务输出的;再者,升级某个组件时不得不把整个容器重建,影响面太大。

用 Docker Compose 编排,每个服务独立成容器,通过自定义网络互相通信,启动顺序用depends_on控制,日志分开查看,升级时只重建对应的那个容器。更重要的是,配置文件可以挂载到宿主机上,改完配置重启容器即可生效,不用进容器内部折腾。这套方案在测试环境和生产环境之间迁移时,只需要把 compose 文件和配置目录拷过去,改一下环境变量就能跑起来。

2.3 网络模式与端口规划

容器之间通信走的是 Compose 自动创建的桥接网络,服务名就是主机名,比如网络服务器连接数据库时直接用postgres作为地址即可。对外暴露的端口需要仔细规划,避免和宿主机上已有服务冲突。

我一般这样分配:网关桥接的 UDP 端口用 1700,这是 LoRaWAN 网关常用的默认端口;MQTT 用 1883;Web 管理界面用 8080;网络服务器的 API 端口用 8000。如果宿主机上已经有其他服务占用了这些端口,可以在 compose 文件里改成其他值,但要注意容器内部的服务发现用的是容器端口,不是映射后的宿主机端口,这两者不要搞混。

注意:网关桥接的 UDP 端口必须和网关设备上配置的目标端口一致,否则网关发过来的数据包会被直接丢弃,而且不会有任何日志提示,这是新手最容易卡住的地方。

3. 环境准备与基础配置实操

3.1 宿主机环境检查与依赖安装

在开始之前,先确认宿主机上已经装好了 Docker 和 Docker Compose。用下面的命令检查版本,建议 Docker 版本不低于 20.10,Compose 版本不低于 2.0。

docker --version docker compose version

如果还没装,可以参考官方文档安装,这里不展开。需要留意的是,某些精简版系统默认没有安装curl和ca-certificates,拉取镜像时会报证书错误,提前装好能省不少事。

sudo apt update sudo apt install -y curl ca-certificates gnupg

接下来创建一个工作目录,所有配置文件和挂载卷都放在这里面,方便管理和备份。

mkdir -p ~/chirpstack-docker cd ~/chirpstack-docker mkdir -p config postgres-data redis-data mosquitto-config mosquitto-data

目录结构规划清楚之后,后面挂载配置文件时路径不容易写错。postgres-data和redis-data用来持久化数据,即使容器删了重新创建,数据也不会丢。mosquitto-config放 MQTT 的配置文件,config放 ChirpStack 各组件的配置文件。

3.2 数据库与缓存服务的配置要点

PostgreSQL 容器启动时需要设置初始密码和数据库名,这些通过环境变量传入。我习惯把密码放在.env文件里,compose 文件引用变量,这样密码不会硬编码在配置里,也方便不同环境切换。

# .env 文件内容 POSTGRES_PASSWORD=your_strong_password_here POSTGRES_DB=chirpstack POSTGRES_USER=chirpstack

Redis 的配置相对简单,默认不需要密码,但如果宿主机暴露在公网环境,建议设置密码并限制访问来源。在测试环境里,我一般只映射到本地回环地址,不对外暴露。

# docker-compose.yml 片段 services: postgres: image: postgres:14-alpine environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} volumes: - ./postgres-data:/var/lib/postgresql/data restart: unless-stopped redis: image: redis:7-alpine volumes: - ./redis-data:/data restart: unless-stopped

这里用alpine版本的镜像,体积小,启动快。restart: unless-stopped保证容器意外退出时会自动重启,但手动停止后不会自己起来,这个策略在调试时比较友好。

3.3 MQTT 代理的配置与权限控制

Mosquitto 作为消息中枢,需要配置允许匿名访问或者设置用户名密码。测试环境为了省事可以开匿名,但生产环境一定要配认证。配置文件放在mosquitto-config/mosquitto.conf。

listener 1883 allow_anonymous true persistence true persistence_location /mosquitto/data/ log_dest stdout

如果要用密码认证,需要先用mosquitto_passwd工具生成密码文件,然后在配置里指定password_file路径,并把allow_anonymous设为false。这一步在容器里操作稍微麻烦,可以先用一个临时容器生成密码文件再挂载进去。

docker run --rm -it -v $(pwd)/mosquitto-config:/mosquitto/config eclipse-mosquitto:2 \ mosquitto_passwd -c -b /mosquitto/config/passwd chirpstack your_password

生成的passwd文件会出现在mosquitto-config目录下,然后在配置文件里加上password_file /mosquitto/config/passwd即可。注意密码文件里的用户名和密码要和后面 ChirpStack 配置里填的一致,否则会出现连接被拒绝但日志不明确的情况。

4. ChirpStack 各组件配置详解

4.1 网络服务器配置文件逐项说明

网络服务器的配置文件chirpstack-network-server.toml是核心,里面涉及数据库连接、Redis 地址、网关桥接的 MQTT 主题等。下面是我实际使用的一份配置,关键项都加了注释。

[postgresql] dsn="postgres://chirpstack:your_strong_password_here@postgres:5432/chirpstack?sslmode=disable" [redis] url="redis://redis:6379" [network_server] net_id="000000" [network_server.band] name="CN470" [network_server.gateway.backend] type="mqtt" [network_server.gateway.backend.mqtt] server="tcp://mosquitto:1883" username="chirpstack" password="your_password"

net_id是网络标识符,LoRaWAN 里用来区分不同网络,测试环境随便填一个十六进制值即可,但要注意不能和别的网络冲突。band.name指定区域频段,这个必须和你的网关以及终端设备匹配,选错了设备根本入不了网。国内常用的有 CN470,欧洲用 EU868,北美用 US915,按实际情况选。

数据库连接串里的sslmode=disable是因为容器内部通信不需要 TLS,如果数据库在外部且要求加密连接,这里要改成require并配置证书。MQTT 的用户名密码要和 Mosquitto 里设置的一致,否则网络服务器连不上代理,日志里会报连接被拒绝。

4.2 应用服务器与 Web 界面的联动配置

应用服务器的配置文件chirpstack-application-server.toml需要指向同一个数据库和 Redis,同时要配置和网络服务器之间的通信方式。两者之间通过 MQTT 交换数据,所以应用服务器也要连到同一个 Mosquitto。

[postgresql] dsn="postgres://chirpstack:your_strong_password_here@postgres:5432/chirpstack?sslmode=disable" [redis] url="redis://redis:6379" [application_server] [application_server.integration] [application_server.integration.mqtt] server="tcp://mosquitto:1883" username="chirpstack" password="your_password" [application_server.api] [application_server.api.public] host="0.0.0.0" port=8001 [application_server.external_api] [application_server.external_api.jwt_secret]

Web 界面容器需要配置后端 API 的地址,通常通过环境变量传入。在 compose 文件里这样写:

chirpstack-application-server: image: chirpstack/chirpstack-application-server:3 volumes: - ./config/chirpstack-application-server.toml:/etc/chirpstack-application-server/chirpstack-application-server.toml ports: - "8080:8080" depends_on: - postgres - redis - mosquitto restart: unless-stopped

Web 界面默认监听 8080,映射到宿主机后通过浏览器访问http://宿主机IP:8080就能看到登录页。初始账号是admin,密码也是admin,第一次登录后务必修改。

4.3 网关桥接服务的配置陷阱

网关桥接服务负责把网关的 UDP 数据转成 MQTT 消息,配置里要指定网络服务器的 MQTT 地址和主题前缀。这里有个容易忽略的点:主题前缀必须和网络服务器配置里的保持一致,否则消息发出去没人收。

[general] [general.log_level] level="info" [backend] type="mqtt" [backend.mqtt] server="tcp://mosquitto:1883" username="chirpstack" password="your_password" [integration] marshaler="json" [network_server] [network_server.mqtt] server="tcp://mosquitto:1883" username="chirpstack" password="your_password"

UDP 监听端口在 compose 文件里映射,默认是 1700。如果网关配置的目标端口不是 1700,要么改网关配置,要么改映射。我建议统一用 1700,减少混乱。

chirpstack-gateway-bridge: image: chirpstack/chirpstack-gateway-bridge:3 volumes: - ./config/chirpstack-gateway-bridge.toml:/etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml ports: - "1700:1700/udp" depends_on: - mosquitto restart: unless-stopped

注意:UDP 端口映射一定要写/udp,不写的话默认是 TCP,网关发过来的 UDP 包根本进不来,而且容器日志里看不到任何异常,只会表现为“网关在线但收不到数据”。

5. 完整部署流程与验证方法

5.1 启动顺序与数据库初始化

所有配置文件准备好之后,用docker compose up -d启动。但这里有个顺序问题:网络服务器和应用服务器启动时会尝试连接数据库并执行迁移,如果数据库还没准备好,它们会报错退出。虽然restart策略会让它们反复重试,但日志里会有一堆吓人的错误信息。

我的做法是先单独启动数据库和 Redis,等它们健康检查通过后再启动其余服务。

docker compose up -d postgres redis sleep 10 docker compose up -d

数据库初始化完成后,ChirpStack 会自动创建所需的表结构。可以用下面的命令确认表是否建好:

docker compose exec postgres psql -U chirpstack -d chirpstack -c "\dt"

如果看到一堆以device、gateway、application开头的表,说明初始化成功。如果报错说数据库不存在,检查.env文件里的POSTGRES_DB是否和配置文件里的连接串一致。

5.2 服务健康状态检查与日志排查

全部启动后,用docker compose ps查看各容器状态。正常情况应该都是Up或者Up (healthy)。如果有容器反复重启,用docker compose logs 服务名查看具体报错。

docker compose ps docker compose logs chirpstack-network-server --tail 50

网络服务器启动成功的标志是日志里出现starting ChirpStack Network Server并且没有后续的connection refused错误。应用服务器类似,看到starting ChirpStack Application Server且监听端口正常即可。

如果某个服务一直连不上数据库,重点检查三点:数据库容器是否真的起来了、连接串里的主机名是否用的是服务名、密码是否和.env里一致。我遇到过因为密码里有特殊字符导致连接串解析错误的情况,后来把密码改成纯字母数字组合就正常了。

5.3 网关接入与设备入网验证

服务全部跑起来之后,在 Web 界面里创建一个网关记录,填入网关的 EUI,然后配置网关设备让它指向宿主机的 IP 和 1700 端口。网关上线后,在 Web 界面的网关列表里应该能看到状态变为在线,并且有心跳数据。

接着创建一个应用和一个设备,设备入网方式可以选择 OTAA 或 ABP。OTAA 需要设备支持入网请求流程,ABP 则是直接写死会话密钥。测试阶段我建议先用 ABP,省去入网交互的排查环节,等链路通了再试 OTAA。

设备创建好后,在应用里配置一个 HTTP 集成或者 MQTT 集成,用来接收上行数据。如果一切正常,设备发送数据后,你配置的接收端应该能收到 JSON 格式的载荷。如果收不到,按下面的顺序排查:网关是否在线、网关桥接日志是否有 UDP 包进来、网络服务器日志是否有去重和解密记录、应用服务器日志是否有转发动作。

6. 常见问题与排查技巧实录

6.1 容器启动失败与依赖问题速查

下面这张表整理了我在搭建过程中遇到的高频问题,以及对应的排查方向。

现象可能原因排查方法
网络服务器反复重启数据库未就绪或连接串错误查看日志中的连接错误,确认数据库容器状态
网关显示在线但无数据UDP 端口未映射或映射为 TCP检查 compose 文件中端口是否带/udp
设备入网失败区域频段配置不匹配核对网关、网络服务器、设备的频段设置
MQTT 连接被拒绝用户名密码不一致检查 Mosquitto 密码文件和各服务配置
Web 界面无法登录数据库未初始化完成等待数据库就绪后重启应用服务器

6.2 数据收不到时的分层排查思路

数据链路涉及多个环节,出问题时不要一头扎进某个服务里翻日志,按层排查效率更高。我的习惯是从下往上查:先确认网关有没有收到射频信号,再看网关桥接有没有把 UDP 转成 MQTT,接着看网络服务器有没有处理,最后看应用服务器有没有转发。

具体操作上,可以用mosquitto_sub订阅网关桥接发布的主题,看有没有原始数据上来。如果这一步就没有,问题在网关到桥接这一段;如果有,再订阅网络服务器转发的主题,逐层缩小范围。

docker compose exec mosquitto mosquitto_sub -t "gateway/#" -v

这个命令能实时看到网关上报的原始消息,对于判断链路是否通畅非常有用。注意订阅主题时要根据实际配置的前缀来写,不同版本的默认主题可能不一样。

6.3 性能调优与资源限制建议

测试环境跑起来之后,如果发现容器占用资源过高,可以给每个服务加上资源限制。PostgreSQL 和 Redis 对内存比较敏感,建议至少各分配 512MB。网络服务器和应用服务器是 CPU 密集型,根据设备数量适当调整。

deploy: resources: limits: memory: 512M reservations: memory: 256M

另外,Redis 的持久化策略可以根据需要调整。如果只是测试,可以关闭 RDB 和 AOF,减少磁盘写入;生产环境则建议开启 AOF,保证意外断电后数据不丢。PostgreSQL 的共享缓冲区参数也可以根据宿主机内存大小适当调大,默认值在容器里偏保守。

我在实际使用中发现,设备数量超过几百台之后,网络服务器的去重窗口会占用较多内存,这时候需要关注 Redis 的内存使用情况,必要时调整去重窗口大小或者升级硬件配置。这些参数在配置文件里都有对应项,调整后重启容器即可生效。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进