NEXT HMIDocs
Get started / Installing and running NEXT HMI

Installing and running NEXT HMI

NEXT HMI ships in two distributable forms:

  1. Docker image — single container; runtime home (and any projects under it) mounted as a volume. Pull it from ghcr.io, or download it as an offline installer zip from the release.
  2. Portable Mac/Windows/Linux binary — unzip + double-click. No installer, no admin rights, no Python or Node toolchain on the host.

Both formats run the same backend over the same on-disk layout. Move a project from one runtime to another with a zip export/import, a LAN push, or by copying its folder and pointing the destination runtime at it.

Before putting NEXT HMI on any network, read Network placement and threat model. A running project writes to PLCs, and editor access runs authored code.

On-disk model

There are two distinct kinds of state:

  • Runtime home — per-installation bookkeeping (manifest, logs, widget build cache). One per runtime.
  • Project folder — self-contained per-project state (page tree, datasources, widgets, assets, custom components, …). The operator can keep as many as they want, anywhere on disk, and run several at once — each running project is a separate backend instance behind the manager.

The process you launch is the manager (front door, default port 8000). It serves a password-gated dashboard, starts one child backend per running project, and reverse-proxies each project to a browser under /runtime/<slug>/ or /editor/<slug>/. The dashboard is at the origin root; an individual project's HMI is at http://<host>:8000/runtime/<slug>/ and its editor at http://<host>:8000/editor/<slug>/.

Runtime home

<runtime_home>/
  projects.json           manifest: running[], projects[], peers[], defaultProjectId, defaultProjectsRoot
  .manager-auth.json      device-admin password digest, token secret, sign-ins
  .logs/                  rotating application logs
  .widget-build/          compiled custom-widget JS (regenerated)
  .restart-pending        sentinel for /api/system/restart
  .peer-tokens.json       hashed manager peer tokens (no bearer plaintext)
  .mcp-tokens.json        hashed MCP and API tokens
  .peer-trust.json        peer TLS certificates you confirmed when pairing
  .peer-pairings.json     pairing keys for peers you chose to remember (owner-only)
  .peer-identity.json     this runtime's runtime ID, as peers pin it
  .install-id.json        random installation ID used by usage reporting
  .telemetry.json         usage-reporting on/off (absent means on), contact prompt dismissed
  .telemetry-session.json whether the last run stopped cleanly, and its version
  .telemetry-milestones/  a dated file per first time: connected, pageSaved, mcpUsed
  .embedding.json         sites that may show NEXT HMI in a frame (absent means none)
  tls/                    HTTPS setting + generated and uploaded certificates
  .peer-transfer-sender.json    outgoing push transfer phases/progress (no bearer plaintext)
  .peer-transfer-receipts.json  incoming push claims, recovery phases, and receipts
  .peer-transfer-pull.json      outgoing pull phases and receipts

Resolution order (highest precedence first):

  1. NEXTHMI_DATA_DIR env var — used by Docker and any explicit override.
  2. Bootstrap config file:
    • macOS / Linux: ~/.config/nexthmi/runtime.json
    • Windows: %APPDATA%\NextHMI\runtime.json
  3. Dev fallback — <repo>/.dev-runtime-home/, used only when running from a source checkout with neither of the above set, so dev state stays inside the repo tree.
  4. Platform default — ~/Documents/NextHMI/ (or %USERPROFILE%\Documents\NextHMI\ on Windows).

The bootstrap config file is written on first run when nothing was found, so subsequent launches are explicit about which path is active. Because it outranks the dev fallback, a machine where a binary has ever started uses that path from a source checkout too — read the startup banner's Runtime home line rather than assuming .dev-runtime-home/.

The runtime home path is read-only from the UI — to move it, edit the env var or the bootstrap file and restart. defaultProjectsRoot (where new, imported and pulled projects land by default) is unset out of the box and resolves to your Documents folder (%USERPROFILE%\Documents on Windows); nothing is created for it. It is likewise not editable from the UI; every dialog that creates a project lets you type or browse to a different folder, and a permanent change means editing projects.json while the manager is stopped.

Project folder

<project>/
  .backups/               pre-upgrade project zips (installation-local)
  config.json             page tree + project metadata + global settings (incl. mcpEnabled)
  pages/                  one JSON per page
  dialogs/                one JSON per page in the Dialogs folder
  datasources/            one JSON per datasource
  components/             reusable composite components
  translations/           semicolon-separated CSVs, one per group
  themes/                 one JSON per theme (colors / typography / spacing)
  alarms.json             alarm definitions
  alarm_state.json        runtime alarm state (shipped with push/pull)
  recipes.json            recipe definitions and datasets
  recipe_state.json       loaded dataset per recipe type
  users.json              users + groups + access settings
  custom-widgets/         user-authored TSX widgets (compiled on save)
  external-libraries/     JS modules exposed via the import map
  assets/
    icons/                SVGs available to icon fields
    images/               images available to image fields
    videos/               video files available to video fields
  certs/                  per-project OPC-UA client certs
  historian/              historian config + installation-local database

The backend creates any missing folders on startup. Everything in this tree is user state: an application upgrade never overwrites it. A project written by an older build is brought to the current file format on request, after a backup — see Managing projects.

Managing projects

The manager dashboard (the origin root, behind the device-admin password) is the operator surface:

  • First-run setup — on first launch the dashboard asks for the setup code and a device-admin password of at least 8 characters. Until a password is set, the device prints the code (XXXX-XXXX) in its console and its log at every start; with Docker, docker logs nexthmi shows it. The code works once, and a restart prints a new one. The password is stored hashed in <runtime_home>/.manager-auth.json and gates every subsequent visit. Signing in lasts a day, counted from the moment you sign in and not from your last click: after that the dashboard asks for the password again, and an editor left open on the panel shows Signed out until someone does. Signing out ends the sign-in on the device too; Settings › Security › Sign out everywhere ends every browser's, and changing the password ends every one but your own.
  • Project credentials — a project brings its own users.json and nothing else gates it: a project copied from the bundled seed holds only the anonymous guest user, so it starts and opens with no password of its own. A project whose users.json is missing, unreadable or corrupt shows Credentials unavailable and will not start until the file is repaired.
  • What the password does and does not cover — the device-admin password gates the dashboard and every project's editor (/editor/<slug>/). A running project's live screens (/runtime/<slug>/) are open to anyone who can reach the host, deliberately: that is what an operator panel is. Operating is open too: anyone who can reach the panel can write writable tags, and blocking the button is how a project stops operators changing a value. The install answers on every interface by default, so put the manager on a trusted network and use HTTPS where it is not. What is reachable without a password lists what is open, what each credential adds, and how to place the device on the network.
  • Start / Stop — bring a project up or down. A running project gets its own backend instance and becomes reachable at /runtime/<slug>/ (and /editor/<slug>/); the set of running projects is remembered and auto-resumed after a restart. Projects run side by side — starting or stopping one never touches another.
  • + New project — create at an absolute path, seeded from the bundled template.
  • ⊕ Add existing — register a project folder that is already on disk (a Git clone, a folder copied off another machine).
  • ↑ Import zip — upload a zip exported elsewhere into a new folder.
  • ⇩ Pull from peer — fetch a project from another manager on the LAN.
  • Transfer — send that explicit source project to another manager. Pair once with the destination's existing device-admin password; subsequent transfers use a revocable peer token. The destination path is always a folder directly below its configured projects root. Collisions reject by default; an administrator may instead copy with a new ID or replace a stopped project with backup/rollback. Starting the received project is an explicit opt-in.
  • Export — download the project as <name>.nexthmi.zip.
  • Set as default — mark the project the origin root redirects to, and keep it running.
  • MCP write enabled / disabled — per-project switch for whether the workspace MCP endpoint may write to this project. See AI agents over MCP.
  • Locate… — re-point a manifest entry whose folder has been moved. Shown only on a missing row, which is the only state where it applies. The destination must carry the same project id, so it can re-point the entry but never adopt a different project.
  • Remove — drop the manifest entry (optionally delete the folder too). A project that is running must be stopped first.

Manager transfer defaults to plain HTTP and is intended only for a trusted LAN: the bearer token prevents unauthenticated mutation but does not stop a network observer from intercepting the pairing password, token, or project data. Serve the peer over HTTPS to close that (see Peer transfer over HTTPS), keep peers on a private network either way, and do not expose the peer endpoints to the public Internet. A pairing expires after 90 days without use; Settings › Tokens lists every device paired with this one under Paired devices, where Revoke cuts one off at once, and changing the device-admin password revokes them all. Outgoing connections resolve once and are pinned to a private unicast address; public, mixed, link-local, multicast, and unspecified results are rejected. Loopback peers require the explicit NEXTHMI_ALLOW_LOOPBACK_PEERS=1 deployment override.

Usage reporting

The runtime reports that it is running to https://next-hmi.com/ping — once at start-up, then once an hour while it keeps running. It is the only outgoing connection NEXT HMI makes on its own, and it exists to answer a few questions: how many installations are active, how long they stay up, and whether people get past connecting to a PLC.

Each report carries exactly:

Field Example Where it comes from
installId f1c74a5c… random ID generated on first run, kept in <runtime_home>/.install-id.json
version 1.4.0 the build
edition oss / ee the build
os / osRelease Darwin / 25.6.0 the host platform
python 3.14.6 the bundled interpreter
event start / heartbeat which of the two moments this is
installMethod bundle / docker / source / other the portable download, the Docker image, a source checkout, or none of those
sessionId 9a07e2c1… random ID for this run, new on every start
uptimeS 86400 seconds since this run started
connected / pageSaved / mcpUsed true whether this installation has ever connected to an OPC-UA server, saved a page, or answered an MCP tool call
pages / tags / opcuaConns 11-100 how many pages, variables and OPC-UA client connections all projects have together, as 0, 1-10, 11-100 or 100+
mcpEnabled false whether any project has MCP access switched on
prevExit clean / unclean / none start only: whether the previous run shut down normally, crashed or was killed, or there was none
prevVersion 1.0.0 start only: the version of the previous run, when it was a different one; empty otherwise

Nothing else is sent: no project names, page or variable names, tag values, addresses, user names, or licence data. The sizes are counted on this machine and only the class leaves it. The receiving end stores no IP address. The installation ID is a random number tied to the runtime home, not to the machine — copy the runtime home and both copies report as the same installation; delete .install-id.json and the next start counts as a new one.

A report is best-effort. On a plant network with no route to the Internet it fails silently and the app carries on; nothing waits on it, nothing is queued, and no error is shown.

To turn it off, either:

  • Settings → Usage reporting in the manager dashboard (takes effect immediately, no restart), or
  • set NEXTHMI_TELEMETRY=off in the environment — this wins over the setting and makes the switch read-only, which is the form to use in Docker or a managed deployment. NEXTHMI_TELEMETRY=on forces the opposite the same way.

Once an installation has connected to a PLC and saved a page, the Projects page asks once whether you use NEXT HMI in a machine or product. Tell us about it opens the contact form on next-hmi.com in your browser — with the installation ID filled in while usage reporting is on, so the message can be read next to this installation's reports — and Not now puts the question away for good. The app itself sends nothing either way; only submitting the form does. The same link stays under Settings → Usage reporting.

HTTPS

The manager binds every interface by default and serves plain HTTP, so out of the box the device-admin password, every operator sign-in, and every project edit cross the wire in the clear. A password still gates the dashboard and every editor — that part does not depend on the binding — but anyone who can watch the network reads it on its way past. Turn HTTPS on unless the network is one you trust, or pin the install to NEXTHMI_HOST=127.0.0.1 and reach it some other way.

The dashboard says so itself while that is the case. Open it at an address that is neither HTTPS nor localhost and a notice stands above the project list, on the settings page, and on the sign-in screen, linking to the switch. It follows the address in your URL bar rather than where the server runs — the same distinction the browser makes in What plain HTTP costs the browser below — so reaching a networked install from the machine it runs on shows nothing, while reaching it from a laptop shows the notice. The operator runtime and the editor stay quiet: it is addressed to whoever can act on it.

Only the manager terminates TLS. Project children are spawned on loopback and reached over plain HTTP by the in-process proxy, so nothing else needs configuring; the SPA already switches its WebSocket to wss:// whenever the page itself was loaded over https://. One certificate therefore covers the dashboard, every project's HMI, and every project's editor.

What plain HTTP costs the browser

Encryption is not the only thing at stake. Browsers gate a set of APIs behind a secure context — HTTPS, or localhost / 127.0.0.1, and nothing else. A page served over plain HTTP to a LAN address (http://192.168.1.10:8000) is not a secure context, so those APIs are absent rather than merely restricted, and no permission the operator grants can hand them back.

The reasoning is that plain HTTP has no integrity. Anyone on the path — a compromised router, another device on the same LAN, a transparent proxy — can rewrite the page in flight, and the browser cannot tell injected script from the application's own. Granting such a page the clipboard would grant it to whoever sits on the network, so the browser withholds it from every HTTP page rather than asking.

In NEXT HMI this costs the editor's clipboard: Copy and Paste on widgets, property values and component definitions report that the clipboard needs HTTPS or localhost, and stay unavailable until the page is served over one. Nothing else depends on it — the runtime, bindings, alarms, and the editor itself all work over plain HTTP.

What decides this is the address in the browser's URL bar, not where the server runs. Opening a Docker install at http://localhost:8000 on the host machine is a secure context; opening the same container at http://192.168.1.10:8000 from a laptop is not.

Ports

Enabling HTTPS moves the app to a second port and leaves the first one redirecting, so links made before the switch keep working:

Setting 8000 (NEXTHMI_PORT) 8443 (NEXTHMI_HTTPS_PORT)
HTTPS off (default) the app, over HTTP not bound
HTTPS on 307 to the HTTPS port, path and query intact the app, over TLS

The redirect is deliberately temporary, not a 301: browsers cache a permanent redirect hard enough that turning HTTPS back off would strand the operator on a port nothing listens on. Turning it off unbinds 8443 rather than redirecting downward — a redirect from HTTPS to HTTP is what an attacker wants, and it would need a certificate warning to deliver.

WebSocket handshakes cannot follow a redirect, so the HTTP port closes them. Nothing in the SPA reaches that path: its document is redirected first, and it derives ws:/wss: from the origin it was actually loaded from.

An MCP client is the one caller that has to be re-pointed by hand: most do not follow redirects, so a connector configured against :8000/mcp stops working the moment HTTPS goes on. See AI agents over MCP.

Both ports are published by docker-compose.yml. Override either with NEXTHMI_PORT / NEXTHMI_HTTPS_PORT, or --port / --https-port.

This split applies only to HTTPS turned on in Settings → HTTPS. A deployment that pins its certificate through NEXTHMI_SSL_CERTFILE / NEXTHMI_SSL_KEYFILE keeps serving TLS on NEXTHMI_PORT and binds nothing else: it has been HTTPS since its first boot, so there is no earlier plain-HTTP link to preserve and no toggle that could strand a page — and moving it would break the port mapping the operator already published.

From the manager UI

Settings → HTTPS is the normal route and needs no shell access:

  1. Pick HTTPS on the protocol switch and confirm. Everyone signed in to the dashboard, you included, signs in again over HTTPS: a sign-in made over plain HTTP stops working.
  2. Under Certificate, keep Generated for this device — the manager creates a self-signed certificate covering localhost, this host's name, and its addresses, including the one it is routed to on the network — or choose My own certificate and upload a PEM certificate and unencrypted PEM private key. Mismatched or passphrase-protected keys are rejected at upload, not at the next startup.
  3. The manager stops running projects, restarts, and the page reopens itself on the new protocol — on port 8443, see Ports above. Projects resume on their own.

Both certificates are kept side by side under <runtime_home>/tls/, so switching between generated and uploaded needs no re-upload. The private keys are written 0600.

A generated certificate is not signed by any authority, so browsers warn once per machine until someone accepts it. That warning is the cost of not needing a CA; the connection is encrypted either way, which a plain-HTTP one is not.

It also names the addresses the device had when it was generated. If the device later answers on a different one — a new DHCP lease, a move to another network — the warning comes back for the address URL, and Regenerate under Certificate issues a fresh one for the address it has now. A static address or a DHCP reservation avoids the round trip. An already-generated certificate is never rewritten on its own.

Upload your own key only over an HTTPS page — the UI warns when the page is on HTTP, since the key would otherwise cross the network in the clear. Turn on HTTPS with the generated certificate first, then upload over that.

Built-in TLS from the environment

NEXTHMI_SSL_* overrides the UI setting entirely, and the HTTPS section then reports itself read-only. Use it for Docker, or wherever configuration is owned by the deployment rather than the operator. Both variables are required together, and both files must exist at startup or the launcher refuses to bind:

Variable Purpose
NEXTHMI_SSL_CERTFILE PEM certificate chain (leaf first).
NEXTHMI_SSL_KEYFILE PEM private key.
NEXTHMI_SSL_KEYFILE_PASSWORD Passphrase, only if the key is encrypted.
export NEXTHMI_SSL_CERTFILE=/etc/nexthmi/tls/fullchain.pem
export NEXTHMI_SSL_KEYFILE=/etc/nexthmi/tls/privkey.pem
./nexthmi

The banner then prints an https:// URL. In Docker, bind-mount the cert directory read-only and set the same variables; the files must be readable by the user the app runs as.

Certificate renewal is not automated in either route — the process reads its certificate once at startup, so restart the manager after a renewal (for an uploaded certificate, re-upload it and pick the protocol again).

Behind a terminating proxy

Caddy, nginx, or Traefik in front of the manager is the better option where one already exists: it handles renewal. Set NEXTHMI_HOST=127.0.0.1 so the manager answers the proxy alone and nothing reaches it directly, leave NEXTHMI_SSL_* unset, and have the proxy send X-Forwarded-Proto and pass the browser's Host header through unchanged. Caddy and Traefik do that by default; nginx needs proxy_set_header Host $http_host; ($host drops the port, so a proxy on :8443 would no longer match). Without it the manager cannot tell the browser's own address from another website's and refuses every change and every live connection. Leave the Content-Security-Policy and X-Frame-Options response headers as the manager sends them; to show NEXT HMI inside another site, list that site instead (see Showing NEXT HMI inside another site). A proxy on the same host is trusted out of the box; one on another host needs its address allow-listed:

Variable Default Purpose
NEXTHMI_FORWARDED_ALLOW_IPS 127.0.0.1 Comma-separated proxy addresses whose X-Forwarded-* headers are trusted.

Set this to the proxy's address, never * on an untrusted network — any client that can reach the manager directly could then dictate the scheme and client IP the backend believes it is seeing.

Peer transfer over HTTPS

Manager-to-manager transfer speaks whichever protocol the peer is configured with. The transfer dialog starts on HTTPS, port 8443; a protocol can also be stored on a manual peer entry. A runtime serving TLS advertises scheme=https in its mDNS record, so picking a discovered peer fills its protocol and port in for you.

Peer certificates are trusted because you compared them, not through a CA, because a plant LAN rarely has one. The transfer dialog pairs in three steps — Verify device → Sign in → Send (Fetch when pulling):

  1. Verify device. The manager connects to the peer without sending anything and shows its name, address, runtime ID and the SHA-256 fingerprint of the certificate it presents. On the peer, open Settings › HTTPS and compare that fingerprint with its SHA-256. Only It matches, continue goes on; It doesn’t match stops, and nothing has been sent.
  2. Sign in. Only now is the peer's device-admin password asked for. The manager checks, during the TLS handshake of the pairing request itself, that the peer still presents exactly the certificate you confirmed; otherwise no password leaves this manager. The confirmed certificate is then pinned in <runtime_home>/.peer-trust.json, by the address you used and by the peer's runtime ID.
  3. Send. Every later connection to that peer must present the same certificate. It is the sole trust anchor, so a mismatch aborts the TLS handshake before the bearer token or any project bytes reach the socket.

The pin follows the device rather than its address: the same device at a new address is recognised by its certificate and keeps its pin, while a different certificate at a pinned address, or claiming a pinned runtime ID, stops pairing with Certificate changed: pairing stopped — never a silent re-pin.

A certificate pinned by a release from before this confirmation step was pinned silently, on first contact. The first pairing after the upgrade shows its fingerprint to compare, like a new device, and confirming it then records the peer's runtime ID.

Remember this device (off by default) keeps the pairing key the peer issues, in <runtime_home>/.peer-pairings.json (readable by the manager's user only), so the next transfer to the same device needs no password. The key is only ever sent over the certificate it was paired over. When the peer no longer accepts it — revoked under its Settings › Tokens, unused for 90 days, or its device-admin password changed — the dialog says so (hmi-line3 no longer accepts this device), and Pair again drops the key and asks for the password. Forgetting the device's pinned certificate (below) drops its key too.

A cloned runtime home. The runtime ID lives in <runtime_home>/.peer-identity.json. A runtime home copied to another machine — a cloned VM or disk image — carries it along, and the two devices then hide each other from discovery and cannot pair: the dialog stops with This device has the same identity as the one you are pairing with. Delete .peer-identity.json in the copy's runtime home and restart it; a new runtime ID is made on the next start.

A peer that has ever been reached over HTTPS is never contacted over plain HTTP again, even if it later advertises otherwise — mDNS records are unauthenticated and would otherwise be a downgrade path.

A peer without HTTPS. The dialog refuses to pair with it — Pairing needs HTTPS on the other device — because the password would cross the network unencrypted. Turn HTTPS on there (Settings › HTTPS, choose HTTPS; it restarts on its HTTPS port, 8443 unless changed), then verify the device again with protocol HTTPS and that port. On a network that is genuinely closed to other devices, Isolated network without HTTPS? lets you pair anyway after ticking I understand the password is sent unencrypted, and this network is closed to other devices. The dialog then says Unencrypted connection: the certificate cannot be verified. and shows the first step as Not verified (HTTP). Over plain HTTP no pairing key is remembered.

Renewing a peer's certificate breaks transfers to it on purpose: a certificate that changed without warning looks exactly like an interception. Confirm the new certificate is genuine, then drop the pin — Forget pinned certificate and verify again in the transfer dialog, which drops every pin of that device, and its remembered pairing key, and shows the new certificate to compare — or:

curl -X DELETE "https://<manager>/api/manager/peers/trust?host=<peer>&port=8000"

GET /api/manager/peers/trust lists the current pins and their fingerprints.

Peer discovery itself (mDNS) is unauthenticated regardless of protocol — it advertises which runtimes exist, nothing more, and anything on the LAN can advertise itself under any name. That is why the certificate is compared before the password is typed, whether the peer was discovered or entered by hand.

What TLS covers

The manager session cookie is marked Secure automatically on requests that arrive over HTTPS (directly or via a trusted proxy), and stays unmarked on plain HTTP so local installs keep working. The restart that turns HTTPS on in Settings → HTTPS ends every dashboard sign-in: a cookie set over plain HTTP is not Secure, has crossed the network readable, and the browser would keep sending it to the HTTP port.

Docker

Three ways to get the same image. Each starts it on port 8000 (and 8443 once HTTPS is on) with the runtime home mounted as /data.

From the registry

docker run -d --name nexthmi -p 8000:8000 -p 8443:8443 \
  -v nexthmi-data:/data ghcr.io/mpnvdlee/next-hmi:latest

The image is multi-arch (linux/amd64 + linux/arm64). Swap :latest for a version tag such as :1.0.1 to pin a release.

Open http://localhost:8000 and set the device-admin password when the manager dashboard prompts. It asks for the setup code first: docker logs nexthmi shows it. On first boot the bundled seed project is registered and started; open its HMI or editor from the dashboard. The project needs no password of its own.

Reaching the container from another machine works the same way, with one exception: http://192.168.1.10:8000 is not a secure context, so the browser withholds the editor's clipboard there. See What plain HTTP costs the browser.

As a file download, without a registry

For a host that cannot reach ghcr.io, every release carries an offline installer per architecture, nexthmi-community-docker-linux-x64-<version>.zip and nexthmi-community-docker-linux-arm64-<version>.zip: the image as a file, a compose file that restarts the container with the host, and install.sh. Copy it over, unzip it and run:

./install.sh

It loads the image and starts it, with ./project-data beside it as /data. README.txt in the zip covers stopping, logs and updating.

From a clone of the repo

docker compose up -d

docker-compose.yml at the repo root builds the image and binds ./project-data to /data. That bind-mount carries both the runtime home (projects.json, .logs/, .widget-build/) AND any project folders the operator places under it. Projects with absolute paths outside /data need their own bind-mounts, writable by the user the app runs as.

The user inside the container

The app runs as the unprivileged user nexthmi, uid and gid 10001, not as root. The container starts as root only to give /data to that user, then drops to it. The first start after an upgrade from a release that ran as root changes the owner of every file in the volume; later starts find nothing left to change. On the host, the files in ./project-data therefore belong to uid 10001.

Anything else you bind-mount has to suit that user too, since the container leaves it alone: a project folder outside /data must be writable by uid 10001, and a certificate pinned with NEXTHMI_SSL_CERTFILE / NEXTHMI_SSL_KEYFILE readable. Give it over on the host:

sudo chown -R 10001:10001 /path/on/the/host

To start the container without root at all, run it as that user yourself, docker run --user 10001:10001 …. Nothing is handed over then, so /data has to be writable by it already; a new named volume is.

The enterprise build still runs as root: its license is bound to the host's hardware UUID, which only root can read.

Discovery note: mDNS requires host networking. Without it (default bridge mode) the runtime won't appear on peer pickers — operators can still use manual host:port entries.

Environment variables

Variable Default Purpose
NEXTHMI_DATA_DIR /data (in image) Runtime home. Pins the path; the Admin UI shows it read-only when this is set.
NEXTHMI_FRONTEND_DIST /app/frontend/dist Compiled SPA. Triggers the templated index.html route.
NEXTHMI_WIDGET_BUILD_DIR <runtime_home>/.widget-build Compiled custom-widget output.
NEXTHMI_VALIDATION_SWEEP on Set to off to skip the startup page-validation sweep.
NEXTHMI_TELEMETRY on Set to off to stop the install-count ping and make the Settings switch read-only. See Usage reporting.
NEXTHMI_HOST unset Address the manager binds. Every interface, IPv4 and IPv6, by default; set 127.0.0.1 to answer this machine only — a reverse proxy on it, or nothing at all. Setting 0.0.0.0 binds IPv4 alone, which refuses http://localhost:8000. See HTTPS.
NEXTHMI_SSL_CERTFILE / NEXTHMI_SSL_KEYFILE unset Serve HTTPS from the manager itself, overriding Settings → HTTPS. See HTTPS.
NEXTHMI_FORWARDED_ALLOW_IPS 127.0.0.1 Proxy addresses whose X-Forwarded-* headers are trusted.
NEXTHMI_MAX_PROJECT_ZIP_MB 500 Cap for zip uploads (export / import / push / pull). Oversize archives are rejected before any bytes hit disk.
WATCHFILES_FORCE_POLLING 1 (in image) Required when a project folder is a bind-mount; native inotify doesn't see host edits.
ESBUILD_BINARY_PATH /usr/local/bin/esbuild Path to the esbuild executable used to compile custom widgets.

Upgrading

  • Registry — docker pull ghcr.io/mpnvdlee/next-hmi:latest, then remove the container and run the same docker run again.
  • File download — unzip the newer installer and run its ./install.sh from the folder that holds your project-data/.
  • Clone — git pull, then docker compose up -d --build.

The data volume is untouched, apart from the owner of its files on the first start after an upgrade from a release that ran as root (see The user inside the container). The backend checks each project's stamped format version on activation and refuses to open one written by a newer build.

Re-seeding

If you delete ./project-data/ (or point the volume at a fresh directory), the manager reseeds from the image's bundled seed project on the next start and registers it. A fresh device-admin password is required again, and the reseeded project starts out with only its guest user. Existing volumes retain their current project credentials unchanged.

Portable binaries

Installing

  1. Download nexthmi-community-<os>-<arch>-<version>.zip from the release.
  2. Unzip anywhere you have write access (Downloads, Desktop, anywhere under your home directory).
  3. Launch:
    • macOS: double-click nexthmi.command.
    • Windows: double-click nexthmi.exe.
    • Linux (x64): run ./nexthmi from a terminal in the unzipped folder. It needs glibc 2.38 or later: Ubuntu 24.04, Debian 13, Fedora 39 or newer.

A terminal window opens, prints the banner, and stays in the foreground.

The banner shows the version, the runtime-home path, the address the default project answers on, and a link to the manager's project list (/projects) for reaching the others. Until a device-admin password is set it also shows the Setup code the first visit asks for. Under Getting started it lists the first steps: open the project list in a browser, enter the setup code and choose the device-admin password on the first visit (and sign in with it after that), then pick a project — Open runs it, Open editor lets you change it. In a terminal that supports it, every URL in the banner is a clickable link.

Open the printed URL in a browser. Those rows name localhost, for the browser on this device. Under On the network stand the two addresses another machine types instead — this device's name, and its address for a network whose name resolution does not carry it — because an install binds every interface by default. Set NEXTHMI_HOST=127.0.0.1 to keep it to this machine only, and the network block goes with it: nothing else is answering. See HTTPS before putting an install on a network you do not control.

On the first launch, set the device-admin password, with the setup code from the banner. That is the only credential the install asks for: the seeded project holds only its guest user and opens straight away. Upgrades preserve an existing project's users and passwords byte-for-byte.

The setup code is what claims the install. Until a password exists there is no password to check, so the first-run page asks for the code instead. The device prints it only in this terminal and in its log, and a website cannot read either: a page that points a name of its own at the device gets past its other checks, but not this one. Five wrong codes from one address lock setup from that address for a minute, as five wrong passwords lock sign-in. Anyone who can read the terminal or the log can still claim the install, and it advertises itself over mDNS the whole time it waits, so set the password right after the first start. The code works once; a restart prints a new one.

To stop: focus the terminal window and press Ctrl-C — uvicorn's lifespan shutdown runs and the OPC-UA pool closes cleanly.

Documentation

The zip carries the full documentation as a self-contained HTML site in docs/ beside the executable — open docs/index.html directly, or use the Help button in the editor header, which the manager answers at /help. Builds without a bundled copy (Docker, a source checkout) redirect /help to the public documentation page instead.

First-run gatekeeper prompts

These are unsigned binaries. The OS will ask you to confirm before it runs them once.

macOS — Gatekeeper shows "nexthmi cannot be opened because the developer cannot be verified." Clear the quarantine flag once:

xattr -dr com.apple.quarantine /path/to/nexthmi-macos-arm64/

Then re-launch nexthmi.command. (Alternative: right-click the file → Open → confirm. You have to do that for nexthmi.command and the nexthmi executable next to it.)

Windows — SmartScreen shows "Windows protected your PC." Click "More info" → "Run anyway."

The public builds are not code-signed, and that is the arrangement rather than a gap waiting to be filled: a signed build is part of what a commercial licence buys. It changes nothing about what this build does — the prompts above are a one-time step, not a limitation.

Changing the runtime home

The runtime home path is read-only from the UI. To move it:

  1. Stop the backend.
  2. Edit the bootstrap config file (~/.config/nexthmi/runtime.json on macOS / Linux, %APPDATA%\NextHMI\runtime.json on Windows). Set dataDir to the new path, or delete the file to fall back to the platform default (~/Documents/NextHMI/).
  3. Optionally export NEXTHMI_DATA_DIR to pin it without touching the file — the env var wins over the bootstrap file.
  4. Move the manifest (projects.json) and .logs/ / .widget-build/ into the new location if you want history preserved. Project folders referenced by absolute paths in the manifest don't need to move.
  5. Relaunch.

The bootstrap file is rewritten on first launch when nothing was found, so it's always present after the platform default kicks in.

Changing the default projects root

defaultProjectsRoot in projects.json governs where new / imported / pulled projects land by default. Out of the box it is absent, and resolves to your Documents folder — a place that already exists, so the runtime never creates a folder to hold projects you may keep elsewhere. Neither it nor the runtime home is editable from the UI — Settings → Runtime home displays the runtime home path read-only. Every create / import / pull dialog lets you type or browse to a different destination, so a one-off elsewhere needs no configuration. To change the default permanently, stop the manager, add or edit defaultProjectsRoot in <runtime_home>/projects.json, and relaunch.

Upgrading a binary install

  1. Download the new zip.
  2. Replace the files inside the existing folder (or unzip into a new folder and delete the old one):
    • nexthmi (executable)
    • nexthmi.command (macOS launcher)
    • _internal/ (PyInstaller runtime + bundled assets)
    • version.txt
  3. Re-launch.

The bootstrap config file is outside the binary folder, so it survives the upgrade. The runtime home (and every project tracked in its manifest) lives wherever the bootstrap file points.

Limitations

  • The macOS build is Apple silicon only: nexthmi-community-macos-arm64-<version>.zip. There is no Intel (x64) macOS build.
  • No auto-update. Operators check the release page for new versions.
  • No process-level service install — the binary runs in the foreground of whichever terminal launched it. If you want it persistent, wrap it in launchd (macOS), Task Scheduler (Windows) or a systemd unit (Linux) yourself.