# 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.
