First ThingsBoard POC on Proxmox
12 March 2026 · 2 min
#thingsboard,#proxmox,#docker,#lxc
The requirement was simple on paper: set up an IoT platform to visualize about a hundred LoRaWAN sensors without paying for a vendor’s managed-cloud version. ThingsBoard Community Edition was the obvious candidate. I wanted to run it on the client’s existing Proxmox infrastructure, without a dedicated VM if possible.
Why LXC rather than a VM?
An LXC container uses ten times less RAM than an equivalent Debian VM, and startup is instant. The client already has about ten containers in production on the cluster; staying consistent with the existing pattern simplifies maintenance.
The only catch: ThingsBoard CE wants Docker inside. So LXC nesting (nesting=1, keyctl=1) must be enabled on the container on the Proxmox side, and cgroup v2 interferes a bit with the iptables embedded by Docker. Nothing blocking.
The initial docker-compose
To avoid wasting time debugging a bare-metal install, I started with the adapted official compose:
services:
thingsboard:
image: thingsboard/tb-postgres:3.7.0
ports:
- "8080:9090"
- "1883:1883"
- "5683:5683/udp"
environment:
TB_QUEUE_TYPE: in-memory
INSTALL_TB: "true"
LOAD_DEMO: "true"
volumes:
- tb-data:/data
- tb-logs:/var/log/thingsboard
restart: unless-stopped
volumes:
tb-data:
tb-logs: in-memory for the queue is more than enough at this stage — no need to bring in Kafka for a POC. LOAD_DEMO creates the demo tenant, handy to validate the UI without any sensor integration. Once the real devices arrive, we switch to LOAD_DEMO: "false" and take a clean dump of the compose.
What got stuck
Three things I hadn’t anticipated:
- MQTT port 1883 squatted by mosquitto on the host. I’d forgotten another container was already exposing MQTT. Quick fix:
1884:1883on the ThingsBoard side, and an MQTT reverse proxy on the HAProxy side to route based on client_id. - RAM consumption climbs to 2.3 GB at startup with the embedded Postgres. Initial LXC sizing at 1.5 GB = OOM-killer within the minute. Bumped to 4 GB, it breathes.
- The rule engine doesn’t digest raw Cayenne LPP payloads without a custom JS decoder in the device profile. An hour lost wondering why my temperatures were arriving as
data: "AQEAFw==". Note for later: decode on the ChirpStack side before forwarding.
What’s next
POC validated, the client is signing for a deployment of 600 sensors across 12 sites. Next step: externalize Postgres, switch the queue to RabbitMQ, and write a more serious article on real sizing.
$ gtb react
Did this article resonate?
$ gtb comments --tail
Comments
// loading…