Setting up Rumqttd: a Rust-based MQTT broker
I've been looking into building some WiFi-enabled embedded Rust hardware projects recently. When creating projects where the devices might need to communicate to each other, MQTT can be a really natural choice. MQTT is light-weight, and makes it easy to connect new devices without them needing to be directly aware of each other. Instead, your devices publish and subscribe to messages from an MQTT broker. While there are a lot of brokers out there, I had a few requirements for the one I wanted to use for communicating with my embedded projects:
- It needs to support self-hosting. My goal for most of my IoT projects is to keep them as isolated to my local network as possible. If I want anything I build to be accessible to me outside of my home network, I can use Tailscale. I also want the services involved to be cheap/free if I can. Hardware is already an expensive hobby, so not paying for a bunch of services to improve it is ideal. Both of these factors mean that the many public or cloud-based MQTT brokers won't work for what I want.
- I wanted it written in Rust (or at least a language I know well). I'm doing as much of the embedded development as I can in Rust. I wanted my MQTT broker to keep that pattern going. Keeping the languages limited to ones I have a good working knowledge of, I can make sure I feel comfortable debugging and contributing back to the project when things go wrong.
Luckily, I found rumqttd
a Rust-based MQTT broker. It even has a corresponding MQTT client rumqttc
(though rumqttc
will not work for no-std
Rust projects at this time, so it's not a great fit for most embedded use cases).
Setting up rumqttd
using the container image
The way I've setup rumqttd
locally is through the Docker image provided in the repository. The instructions in the repo suggest you can pull the image and tell it to use a default config file with a command like docker run -p 1883:1883 -p 1884:1884 -it bytebeamio/rumqttd -c rumqttd.toml
. However, I've found that this doesn't seem to work, causing rumqttd
to panic. Instead, I recommend cloning and building the image locally.
- Clone the
rumqtt
image from GitHub. I like to use the GitHub CLI where the command isgh repo clone bytebeamio/rumqtt
, but you can usegit clone https://github.com/bytebeamio/rumqtt.git
to do the same thing. - Build the image locally (instructions here are for Docker, but any service that can build a Docker container should work). In the root of the
rumqtt
repo, rundocker build -t rumqttd .
(you can swap therumqttd
for whatever tag you want for the image). - When that's done, the command they suggest in the repo for use with the pre-built image should work (but with the image name swapped for what we called our image):
docker run -p 1883:1883 -p 1884:1884 -it rumqttd -c rumqttd.toml
(again swappingrumqttd
for the tag you set in step 2). If everything works, ASCII art will spell outRUMQTTD
in the terminal. If you connect an MQTT client to your local instance and subscribe/post through the server, your terminal will log info about each one. If you want to leave the container running in the background, you can use Ctrl+p then Ctrl + q. Otherwise you can stop the container with Ctrl+c.
If you don't want to use containers to run your server, I'd suggest following the instructions in the repo to get it up and running with either cargo install
or cargo run
. I've not tested either of these methods, but am pretty confident that either option should work as long as you pass a config file.
Connecting clients
You'll note that in the Docker command above, 2 ports are exposed, 1883 and 1884. This is because the default config from the rumqttd
repo sets up endpoints for 2 common MQTT versions: MQTT v4.1 on port 1883, and MQTT v5 on port 1884. So if you were connecting a client compatible with MQTT v4 from your computer running your broker, you'd point set the broker URL to http://localhost:1883
. If you were doing the same but with an MQTT v5 client, you'd use http://localhost:1884
for the broker.
Supporting both MQTT v4 & MQTT v5 allows your server to be compatible with most Rust-based MQTT clients. For instance, in no-std
& std
environments, rust_mqtt
, supports MQTT v5, but not v4. On the other hand, rumqttc
, which only works in std
environments, supports both MQTT v4 and MQTT v5, but the v4 implementation is more robust. Unfortunately, at the time of this writing, the MQTT client provided by esp-rs/esp-idf-svc
is only compatible with MQTT v3. So if writing std
applications for espressif chips, you should either look into a different MQTT client library, or find an MQTT broker that supports MQTT v3.
It's also worth calling out that the v4 & v5 brokers feature fully separate queues. So if you send a message to the v5 broker, clients listening to the v4 broker will not receive that message and vice-versa. You might be able to setup the bridge to relay messages across the versions, but I've not looked into it.
Updating the log level in a running broker
When writing custom clients and debugging connections, it can be useful to adjust the log level from the broker service is actively running. The default config provided in the repo provides an endpoint for this at port :3030
. However, following the instructions from earlier, we're not yet exposing this endpoint outside of the container. To do that, we first need to add that port to our docker run
command:
docker run -p 1883:1883 -p 1884:1884 -p 3030:3030 -it rumqttd -c rumqttd.toml
After this, we can send a post request to http://localhost:3030/logs
with a new log level at any time.
e.g. if we wanted to get trace
level logs, we could make request like this:
# sends a post request to localhost:3030 with a content-type of text/plain and a body of rumqttd=trace
curl -H "Content-Type: text/plain" -d "rumqttd=trace" http://localhost:3030/logs
The docs suggest that you can send any valid Tokio tracing-subscriber EnvFilter value. However, for most of the debugging I need when testing MQTT clients, managing the rumqttd
log levels is sufficient.