Help with ChirpStack v4 Docker config for MultiTech gateway using 2 subbands (US915_2 & US915_3)

I’m struggling to properly configure a MultiTech 16-channel (8x2) gateway on ChirpStack v4 using Docker. The gateway is set up to use subbands 2 and 3 (US915_2 and US915_3) .

1. Gateway Bridge Configuration

Because the gateway is listening on a single UDP port (1700) , I’m unsure whether I need:

  • One chirpstack-gateway-bridge instance with wildcard topics , e.g.:
us915_+/gateway/{{ .GatewayID }}/event/{{ .EventType }}

or

  • Two separate chirpstack-gateway-bridge containers , each with specific topic prefixes (us915_2/… and us915_3/… )?

When I try using the wildcard topic with a single bridge, only one of the two gateway EUIs appears correctly in the ChirpStack web interface—even though both gateways are transmitting (confirmed via packet capture).

If I launch two separate chirpstack-gateway-bridge instances with different topic prefixes, both gateways show up, but they are sharing the same UDP port (1700). That feels wrong or at least like something that might cause conflicts.

Here’s an example of what I’m doing (simplified for clarity):

# First Bridge
chirpstack-gateway-bridge:
  image: chirpstack/chirpstack-gateway-bridge:4
  ports:
    - "1700:1700/udp"
  environment:
    - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=us915_2/gateway/{{ .GatewayID }}/event/{{ .EventType }}

# Second Bridge
chirpstack-gateway-bridge2:
  image: chirpstack/chirpstack-gateway-bridge:4
  ports:
    - "1700:1700/udp"  # Same port as above
  environment:
    - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=us915_3/gateway/{{ .GatewayID }}/event/{{ .EventType }}

Is it valid to run two bridge containers on the same UDP port like this?

2. Gateway Hardware Limitation

Even though the MultiTech gateway supports two subbands (2 x 8 channels) , it still only provides one UDP port (1700) for communication. How does that affect subband selection for uplinks?

How can I ensure uplink packets are routed through the correct region config (us915_2 vs. us915_3 )? Should I fake the separation with topic prefixes, even if the gateway only sends from one hardware port?

3. ChirpStack Config

The ChirpStack chirpstack.toml allows enabling multiple regions:

enabled_regions=[
  "us915_2",
  "us915_3",
]

This part seems straightforward, but it’s unclear to me how uplinks are internally routed to the right region if both subbands are active but coming through the same gateway .

4. Final Questions

  • Should I be using two separate gateway bridge containers, or is there a better way?
  • If the gateway only uses one UDP port, how does ChirpStack know which subband the uplink belongs to?
  • Is it normal that the gateway EUI shows up under the wrong region when using only one bridge instance?

Any clarification or configuration examples from others who’ve successfully used 16-channel MultiTech gateways with multiple subbands would be incredibly helpful!

Thanks in advance.

I believe the answer here is to create a new regional .toml file that has the frequencies from both subbands enabled, name it us915_23.toml or something. Then enable us915_23 in your chirpstack.toml and set the gateway bridge to post to to that region prefix.

Liam, thanks for the feedback. I have tried this:

  1. Changed Docker Compose to

chirpstack-gateway-bridge:

  • image: chirpstack/chirpstack-gateway-bridge:4*
  • restart: unless-stopped*
  • ports:*
  •  - "1700:1700/udp"*
    
  • volumes:*
  •  - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge*
    
  • environment:*
  •  - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/event/{{ .EventType }}*
    
  •  - INTEGRATION__MQTT__STATE_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/state/{{ .StateType }}*
    
  •  - INTEGRATION__MQTT__COMMAND_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/command/#*
    
  • depends_on:*
  •  - mosquitto*
    
  1. added region file region_us915_23.toml

However I see the MQTT messages in the log file of the chirpstack-gateway-bridge:
time=“2025-05-08T13:46:54.164235158Z” level=info msg=“integration/mqtt: publishing event” event=up qos=0 topic=us915_23/gateway/00800000a000c662/event/up uplink_id=5191
time=“2025-05-08T13:46:54.965605918Z” level=info msg=“integration/mqtt: publishing event” event=up qos=0 topic=us915_23/gateway/00800000a000c662/event/up uplink_id=29809
time=“2025-05-08T13:46:55.393006815Z” level=info msg=“integration/mqtt: publishing event” event=up qos=0 topic=us915_23/gateway/00800000a000c662/event/up uplink_id=21374
time=“2025-05-08T13:46:55.77465625Z” level=info msg=“integration/mqtt: publishing event” event=up qos=0 topic=us915_23/gateway/00800000a000c662/event/up uplink_id=4789
time=“2025-05-08T13:46:56.116056892Z” level=info msg=“integration/mqtt: publishing event” event=up qos=0

But the web interface shows those as offline

It seems only to accept the predefined region files … Is there another section where I have to update the configurations?

Final observation is that when I add the frequencies from region_us915_3.toml to the region_us915_2.toml then it works … something seems to prevent the new region_us915_23.toml file from being accepted/used. …

Did you add it to the enabled regions array in your chirpstack.toml?

I did here

Share the .toml file of your us915_23, maybe the ‘name’ parameter is incorrect? Or something like that. Never made my own regional file but I know it’s possible.

FYI you can have as many enabled regions as you want in that array without messing anything up. All that dictates is what MQTT topics Chirpstack subscribes to, so what regions it will listen to messages from. It doesn’t dictate the overall region of your Chirpstack deployment if that’s what you’re thinking. It is the MQTT topic prefix in your gateway bridge that dictates what region all uplinks going through that gateway bridge are in.

So the gateway bridge posts to a topic based on the topic prefix you set:
- INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/event/{{ .EventType }}*
Then if that topic prefix is in your enabled regions Chirpstack will subscribe to that topic and assume all messages posted on that topic are from that region and handle them according to the config in the associated region.toml file.

Sorry, was an oversight on my end. I didn’t update the name inside the region file. Now all is working with the region file for 2 sub-bands. Thanks

  1. chirpstack.toml

Enabled regions.

Multiple regions can be enabled simultaneously. Each region must match

the ‘name’ parameter of the region configuration in ‘[[regions]]’.

enabled_regions=[
“us915_23”,

“us915_2”,

“us915_3”,

]

  1. region_us915_23.toml

This file contains an example US915 example (channels 16-23 + 66).

[[regions]]

ID is an use-defined identifier for this region.

id=“us915_23”

Description is a short description for this region.

description=“US915 (channels 16-23 + 66)”

description=“US915 (channels 16-23 + 66) & (channels 24-31 + 67)”

Common-name refers to the common-name of this region as defined by

the LoRa Alliance.

common_name=“US915”

Gateway configuration.

[regions.gateway]

# Force gateways as private.
#
# If enabled, gateways can only be used by devices under the same tenant.
force_gws_private=false


# Gateway backend configuration.
[regions.gateway.backend]

  # The enabled backend type.
  enabled="mqtt"

  # MQTT configuration.
  [regions.gateway.backend.mqtt]

    # Topic prefix.
    #
    # The topic prefix can be used to define the region of the gateway.
    # Note, there is no need to add a trailing '/' to the prefix. The trailing
    # '/' is automatically added to the prefix if it is configured.
    topic_prefix="us915_23"

    # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws)
    server="tcp://$MQTT_BROKER_HOST:1883"

    # Connect with the given username (optional)
    username=""

    # Connect with the given password (optional)
    password=""

    # Quality of service level
    #
    # 0: at most once
    # 1: at least once
    # 2: exactly once
    #
    # Note: an increase of this value will decrease the performance.
    # For more information: https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels
    qos=0

    # Clean session
    #
    # Set the "clean session" flag in the connect message when this client
    # connects to an MQTT broker. By setting this flag you are indicating
    # that no messages saved by the broker for this client should be delivered.
    clean_session=false

    # Client ID
    #
    # Set the client id to be used by this client when connecting to the MQTT
    # broker. A client id must be no longer than 23 characters. If left blank,
    # a random id will be generated by ChirpStack.
    client_id=""

    # Keep alive interval.
    #
    # This defines the maximum time that that should pass without communication
    # between the client and server.
    keep_alive_interval="30s"

    # CA certificate file (optional)
    #
    # Use this when setting up a secure connection (when server uses ssl://...)
    # but the certificate used by the server is not trusted by any CA certificate
    # on the server (e.g. when self generated).
    ca_cert=""

    # TLS certificate file (optional)
    tls_cert=""

    # TLS key file (optional)
    tls_key=""


# Gateway channel configuration.
#
# Note: this configuration is only used in case the gateway is using the
# ChirpStack Concentratord daemon. In any other case, this configuration 
# is ignored.
[[regions.gateway.channels]]
  frequency=905500000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=905700000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=905900000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906100000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906300000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906500000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906700000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906900000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=906200000
  bandwidth=500000
  modulation="LORA"
  spreading_factors=[8]

[[regions.gateway.channels]]
  frequency=907100000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=907300000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=907500000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=907700000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=907900000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=908100000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=908300000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=908500000
  bandwidth=125000
  modulation="LORA"
  spreading_factors=[7, 8, 9, 10]

[[regions.gateway.channels]]
  frequency=907800000
  bandwidth=500000
  modulation="LORA"
  spreading_factors=[8]

Region specific network configuration.

[regions.network]

# Installation margin (dB) used by the ADR engine.
#
# A higher number means that the network-server will keep more margin,
# resulting in a lower data-rate but decreasing the chance that the
# device gets disconnected because it is unable to reach one of the
# surrounded gateways.
installation_margin=10

# RX window (Class-A).
#
# Set this to:
# 0: RX1 / RX2
# 1: RX1 only
# 2: RX2 only
rx_window=0

# RX1 delay (1 - 15 seconds).
rx1_delay=1

# RX1 data-rate offset
rx1_dr_offset=0

# RX2 data-rate
rx2_dr=8

# RX2 frequency (Hz)
rx2_frequency=923300000

# Prefer RX2 on RX1 data-rate less than.
#
# Prefer RX2 over RX1 based on the RX1 data-rate. When the RX1 data-rate
# is smaller than the configured value, then the Network Server will
# first try to schedule the downlink for RX2, failing that (e.g. the gateway
# has already a payload scheduled at the RX2 timing) it will try RX1.
rx2_prefer_on_rx1_dr_lt=0

# Prefer RX2 on link budget.
#
# When the link-budget is better for RX2 than for RX1, the Network Server will first
# try to schedule the downlink in RX2, failing that it will try RX1.
rx2_prefer_on_link_budget=false

# Downlink TX Power (dBm)
#
# When set to -1, the downlink TX Power from the configured band will
# be used.
#
# Please consult the LoRaWAN Regional Parameters and local regulations
# for valid and legal options. Note that the configured TX Power must be
# supported by your gateway(s).
downlink_tx_power=-1

# ADR is disabled.
adr_disabled=false

# Minimum data-rate.
min_dr=0

# Maximum data-rate.
max_dr=3

# Enabled uplink channels.
#
# Use this when ony a sub-set of the by default enabled channels are being
# used. For example when only using the first 8 channels of the US band.
# Note: when left blank / empty array, all channels will be enabled.
enabled_uplink_channels=[16, 17, 18, 19, 20, 21, 22, 23, 66]


# Rejoin-request configuration (LoRaWAN 1.1)
[regions.network.rejoin_request]

  # Request devices to periodically send rejoin-requests.
  enabled=false

  # The device must send a rejoin-request type 0 at least every 2^(max_count_n + 4)
  # uplink messages. Valid values are 0 to 15.
  max_count_n=0

  # The device must send a rejoin-request type 0 at least every 2^(max_time_n + 10)
  # seconds. Valid values are 0 to 15.
  #
  # 0  = roughly 17 minutes
  # 15 = about 1 year
  max_time_n=0


# Class-B configuration.
[regions.network.class_b]

  # Ping-slot data-rate. 
  ping_slot_dr=8

  # Ping-slot frequency (Hz)
  #
  # set this to 0 to use the default frequency plan for the configured region
  # (which could be frequency hopping).
  ping_slot_frequency=0
  1. docker-compose.yml

chirpstack-gateway-bridge:
image: chirpstack/chirpstack-gateway-bridge:4
restart: unless-stopped
ports:
- “1700:1700/udp”
volumes:
- ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge
environment:
- INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/event/{{ .EventType }}
- INTEGRATION__MQTT__STATE_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/state/{{ .StateType }}
- INTEGRATION__MQTT__COMMAND_TOPIC_TEMPLATE=us915_23/gateway/{{ .GatewayID }}/command/#
depends_on:
- mosquitto

Glad to hear it :+1: