# The OpenPLC Runtime container The PLC for this demo runs as a **Docker Compose service** on the shared host `yau-sls-poc-lin001` (`10.0.0.17`). It is deployed from its **own** compose file, `~/openplc-compose.yml` on that host, not from the host's main stack. `openplc-compose.yml` in this folder is a **verbatim copy of the live file**, read from the host on 2026-09-02. The host is canonical for what is running; this copy exists so the deployment is reviewable and re-creatable from the repo. > [!WARNING] > **This container is live control and the demo is running.** CI Server on > `yau-poc-cicore1` (`10.0.0.21`) polls it over Modbus TCP. Do not restart, > recreate or update it as a side effect of other work. ## As-built — verified on the host 2026-09-02 | Item | Value | |---|---| | Container | `openplc-runtime` | | Image | `openplc-runtime-migrated:v4.1.10` (`sha256:1e3bd0e1…a502cbc6`) | | Compose file | `/home/azureuser/openplc-compose.yml` · project `azureuser` | | Published ports | `10.0.0.17:502->502/tcp`, `10.0.0.17:8443->8443/tcp` | | Network | `openplc-net` — **isolated**, not the shared stack network | | Volume | `azureuser-openplc-runtime-data` (external) | | Capabilities | `SYS_NICE`, `SYS_RESOURCE` | | Restart policy | `unless-stopped` · logging `json-file` 10 MB × 3 | | Loaded program | `libplc_1786668930820554523.so`, built 2026-08-14 | The image is **local to the host** — it was produced by `docker commit` during the migration (see `MIGRATION.md`), not pulled from a registry. It cannot be re-pulled; if it is deleted, the compiled program is lost and must be rebuilt and re-uploaded through the OpenPLC Editor (`03-plc/DEPLOY.md`). ## Why each setting matters | Setting | Reason | |---|---| | `cap_add: SYS_NICE, SYS_RESOURCE` | Real-time scheduling. **Omitting these breaks the runtime** — always include them in a recreate. | | `azureuser-openplc-runtime-data:/var/run/runtime` | All runtime state: `restapi.db` (users + program record) and `.env` (JWT secret). Survives recreate; **do not delete the volume**. Declared `external: true` so Compose uses the restored volume instead of creating an empty one. | | `10.0.0.17:502:502` | Modbus TCP for CI Server. **Bound to the LAN address, not `0.0.0.0`.** Modbus has no authentication or encryption, and this host has a public IP — the bind address is the security control. Do not widen it. | | `10.0.0.17:8443:8443` | REST API — the control channel (upload, start/stop). Reachable from the LAN so the OpenPLC Editor can drive it; JWT auth is the only thing in front of it, so the same bind rule applies. | | `networks: openplc-net` | Deliberate isolation from the host's other ~20 containers. The PLC has no reason to reach them, or they it. | | image pinned by tag+digest | Stops a recreate from silently changing the runtime under a tested program. | ## Firewall **UFW on this host is inactive** — inbound filtering is entirely the **Azure Network Security Group**. There is no host firewall rule for 502 or 8443, and adding one is not the mechanism here: 502/8443 are bound to `10.0.0.17`, which is not reachable from outside the VNet, and the NSG governs the public interface. ## If port 502 refuses connections **Two conditions must both hold**, and neither is a container setting: 1. a program is **running** on the runtime, and 2. the Editor project defines a Modbus **Server** so the runtime's `modbus_slave` plugin is enabled — see `03-plc/DEPLOY.md` §A3. Check those before touching the container. A refusal looks like a firewall drop but is not one: Docker DNATs the packet to the container, which returns RST because nothing is bound inside. ## Known issue — PLC scan overruns The runtime logs `[task PLC_TASK] scan overrun` warnings: the 100 ms cycle body occasionally exceeds its period and the task runs at a reduced rate. **100 overruns between 2026-08-19 and 2026-08-28** (roughly one per 4–5 days). Other tasks are unaffected and the demo continues to run. Not yet investigated — this host is shared, so contention with the other containers is a candidate cause.