all articles

APEX #2 — The Foundations: Docker on the APEX Without Getting Thrown Out by OPA

13 October 2026 · 4 min

#distech-controls,#eclypse-apex,#docker,#mqtt,#mosquitto,#serie-apex

ECLYPSE APEX series — episode 2/6 · Previous: #1 A Controller Running Docker, Seriously?

Ever tried to get a couch into an elevator? You turn it, you tilt it, you take the feet off… and eventually it fits. Creating a container on an ECLYPSE APEX is exactly that. Except the elevator is called OPA and it answers “request rejected by administrative policy” with no further explanation.

Today we lay the foundations: how the APEX handles Docker, the rules to follow, and our first real container, Mosquitto. The rest of the series builds on it.

What we have… and what we don’t

Forget your homelab reflexes right away. No terminal here.

What we haveWhat we don’t have
Containerization menu in Eclypse Facilities (images, containers, volumes, networks)No SSH, no docker exec
The REST API v2: /api/rest/v2/services/containerization/...No docker-compose
Persistent named volumesNo logs endpoint (docker logs)
A bridge network between containersNo USB passthrough

Each container is created from a creation JSON, in the Docker Engine API format. It’s a bit rough at first, but you get used to it quickly, and it ends up being very reproducible.

OPA, the nightclub bouncer

A robot bouncer in sunglasses, holding an "OPA" list, raises a hand to refuse entry to a container area

Distech put an OPA authorization plugin in front of the Docker engine. Its job: refuse anything that could endanger the controller. After quite a bit of trial and error, here’s what gets through:

  • ExposedPorts is mandatory in the JSON, otherwise it’s rejected;
  • host ports in the 50xxx range (and not all of them: 50880 was refused on my unit, 50080 accepted);
  • standard internal ports: 1883, 8080, 5432, 6379, 1700, 1880… A Mosquitto on 1884? Rejected, the APEX reserves certain ports for itself;
  • Mounts rather than Binds for volumes;
  • "CpusetCpus": "0" in the HostConfig, which confines your containers to one core: the control logic keeps the others.

Remember this list: when OPA turns you down, it’s almost always one of these points.

ARM64, or the latest tag trap

The APEX runs on ARM64. Most official images are multi-arch, but not all of them, and sometimes not for every tag. A real example: chirpstack/chirpstack:latest was x86 only at the time of my tests, while the fixed version tag went through.

My little trick to test an image without any fuss: create a throwaway container that just prints its help.

{
  "Image": "monimage:tag",
  "Cmd": ["--help"],
  "HostConfig": {
    "CpusetCpus": "0",
    "RestartPolicy": {"Name": "no"},
    "NetworkMode": "bridge"
  }
}

If it ends up exited, the image runs on the APEX. If it stays in restarting, it isn’t compatible (or it doesn’t like --help, but that’s rarer).

All-in-one or multi-container: the story of a U-turn

On the left, a heap of rusty containers tied together with cables; on the right, clean containers lined up on a DIN rail

At first, I was convinced I’d found the right idea: a single custom image bundling ChirpStack, PostgreSQL, Redis and Mosquitto. No networking worries, no startup order, one container to deploy. On paper, it was great.

In real life, it was hell. An update? Rebuild everything. A service crashes? Good luck figuring out which one. And the image weighed a ton.

So I made a U-turn: one service, one container. It means more JSON to write, but each piece lives its own life, updates on its own and gets debugged on its own. The whole rest of the series is built that way.

The best practices that saved my skin

  • Lowercase container names. Names like PostgreSQL or Redis gave me name resolution problems between containers.
  • Communicate via APEX_IP:host_port. It’s the most reliable path for one container to reach another.
  • Logs in a volume. No docker logs? Redirect the output to a file in a volume, and read it with a FileBrowser or an nginx mounted on the same volume.
  • Fixed version tags, always.
  • A port table kept up to date. Trust me, by the fifth container you’ll thank me.
ServiceInternal portAPEX port
Node-RED188050080
ChirpStack (web)808050081
Mosquitto188350083
Zigbee2MQTT (web)808050084
nginx (logs)8050089
ChirpStack REST API809050090
FileBrowser (troubleshooting)8050092
LoRaWAN Gateway Bridge1700/udp50170
Redis637950379
PostgreSQL543250432

First container: Mosquitto

MQTT logo

Mosquitto is the MQTT broker: the train station concourse where everybody crosses paths. Zigbee2MQTT, ChirpStack, Node-RED and Telegraf will all go through it, so logically it’s the first thing we install.

Exploded view of a DIN rail controller: inside, a station labeled "MQTT 1883" where data trains cross paths
  1. Facilities → Containerization → Images → Add: eclipse-mosquitto:2.
  2. Containers → Create, name mosquitto, with this JSON:
{
  "Image": "eclipse-mosquitto:2",
  "ExposedPorts": { "1883/tcp": {} },
  "Cmd": [
    "sh", "-c",
    "echo 'persistence false\nallow_anonymous true\nconnection_messages true\nlog_type all\nlistener 1883' > /mosquitto/config/mosquitto.conf && mosquitto -c /mosquitto/config/mosquitto.conf"
  ],
  "HostConfig": {
    "CpusetCpus": "0",
    "RestartPolicy": { "Name": "unless-stopped", "MaximumRetryCount": 0 },
    "PortBindings": { "1883/tcp": [{ "HostPort": "50083" }] }
  }
}
  1. Start it, then connect MQTT Explorer from your PC to APEX_IP:50083.

Note the trick: no config file to mount, we write it on the fly with a sh -c. We’ll reuse this principle often.

A small caveat that matters: here the broker accepts anonymous connections. That’s perfect on a test bench, but not on a site. In production, enable authentication and restrict network access.

What’s next?

The foundations are poured: you know how to talk to OPA, check an ARM64 image, read logs without SSH, and you have an MQTT broker running in your controller. Not bad for something living on a DIN rail.

In the next episode, we plug in a Zigbee coordinator and make a SONOFF module obey a professional BMS. Things are going to blink!

Links

$ gtb react

Did this article resonate?

$ gtb comments --tail

Comments

// loading…

open/gtb · MIT · hosted in France
visitors github roadmap messages contact
SYS OK 15/18 tools online uptime — Rack temp 21.3 °C