wrps-demo-kit/02-environment/openplc-container.md
Clio Liu 2b8f88d01f feat(env): hosts, access, ports and the PLC container
02-environment merges the old 02-env and 03-containers - both answered the
same question, 'where does this run and how do I reach it', and were split
for no reason.

  README.md              the two hosts, access, ports, secrets, health
                         checks, known issues
  YAU_Linux_Host_Onboarding.md  the Linux host's own brief, copied with a
                         provenance banner: owned by the host owner, not
                         maintained here, and its 'save this as CLAUDE.md'
                         instruction does not apply in this repo
  openplc-container.md   the container as read from the running host
  openplc-compose.yml    verbatim copy of the live file
  MIGRATION.md           moving the container - as-built plus runbook
  secrets.local.md.template  the shape of the git-ignored secrets file

The README leads with the fact that shapes every decision on that machine:
yau-sls-poc-lin001 is a SHARED, LIVE host running ~28 containers for
several projects behind Caddy and Authelia, and this project owns exactly
one of them. Never restart Caddy or Authelia, never publish on 0.0.0.0,
never put growing data on the 62 GB root disk.

Records four known issues rather than leaving them in anyone's head: the
100 ms scan overruns, the 354 MB migration tarballs still sitting on the
host with the JWT secret inside one of them, the runtime image existing in
no registry, and the missing Editor toolchain.

Not carried across: Host_Documentation.md. ~400 lines on ChirpStack,
Forgejo, EQP licensing and the Telegraf fleet - none of it WRPS, owned
elsewhere, and a stale copy here would be worse than a pointer.
2026-09-02 17:24:09 +10:00

4 KiB
Raw Blame History

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-netisolated, 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 45 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.