Skip to content

Deploy

Tutorial. Three ways: a package install for one box with no checkout, Docker Compose for a trial, systemd units for a host that runs test and promoted environments side by side.

Not hardened for the public internet

The MES is designed to sit on a plant network behind a firewall. Put a reverse proxy with TLS in front of it, set the passwords, and do not expose the MCP servers. See security.

From the package, with no checkout

The shortest way to a plant on a machine that has Python and nothing else. The package carries the two lab plant packs, so there is nothing to clone.

pipx install factorysemantics-mes   # or: uv tool install factorysemantics-mes
fsmes demo bottling
pipx install factorysemantics-mes
fsmes demo bottling

Not on PyPI yet

The release on PyPI today, 0.2.0, carries no plant packs, so fsmes demo bottling needs the next one. From a checkout it works now: python -m build --wheel, then install that wheel into a fresh environment.

It says where the plant is — http://127.0.0.1:8010/dashboard — and which account to sign in with. fsmes demo bottling stop puts it down, fsmes demo bottling status says whether it is up, and fsmes demo machining is a second plant on 8020.

What it does, once, on first start: copies the pack out of the package into a per-user directory, generates that plant's eight-hour shift from the seed in its line.json (seconds, and it is not shipped because it is 9.7 MB of CSV a seed already states), writes a fleet file listing the copy, and then runs the same fsmes fleet create and fsmes fleet start a checkout runs. Afterwards it starts straight away.

Everything it writes is under one directory:

Windows %LOCALAPPDATA%\fsmes
Linux $XDG_DATA_HOME/fsmes, or ~/.local/share/fsmes
macOS ~/Library/Application Support/fsmes
Anywhere FSMES_HOME, which moves all of it together

Nothing under the installed package is written to, which is what makes this work on a locked-down plant PC and in a read-only container.

The pack copy is yours to edit — the port, the host, the time zone, the accounts are all in packs/<name>/plant.toml under that directory, and the pack page says what each key means. A later start says your copy differs from the one the package ships and keeps yours; --refresh is the one thing that replaces it.

This is an evaluation plant: it listens on loopback and its accounts are the lab's, with lab passwords in the open. A plant other people reach needs its own [[accounts]] in the pack, a real MES_SECRET_KEY, and the reverse proxy in the warning above. For a plant that is meant to stay up, run it from a systemd unit pointed at that fleet file rather than from fsmes demo.

The AI tab's chat and the floor assistant need an ANTHROPIC_API_KEY (or a local model through Ollama — see the AI page); without one the command says so and every other screen is unaffected.

Compose

git clone https://github.com/factorysemantics/factorysemantics-mes.git
cd factorysemantics-mes
MES_SECRET_KEY="$(openssl rand -hex 32)" MES_ADMIN_PASSWORD='choose-one' \
  docker compose -f docker/docker-compose.yml up --build

That starts PostgreSQL, a one-shot migration and seed job, the API behind nginx on http://localhost:8000, the simulated OPC UA server, the OPC agent, the mock ERP on port 8001 and the ERP sync worker. --scale api=3 adds API replicas; migrations run once in their own container so replicas never race.

To leave twin mode, point MES_OPC_ENDPOINT at your server and MES_TAG_MAP_FILE at the tag map the worksheet produced, and set MES_ERP_MODE to erpnext, file, or off.

The image is ghcr.io/factorysemantics/fsmes (amd64 and arm64), signed with cosign and shipped with an SBOM from each release. It is pushed only after the same approval that publishes the package to PyPI, in the same run — so a tag in the registry means that version was released, not that a build of it happened to succeed. Pin the version tag rather than latest on anything you care about; latest moves with each release.

systemd: test and promoted environments on one host

Two environments run from the same code on the same host; the only difference is a registry file outside the checkout. The unit files and a promote script are in deploy/.

test promoted
Checkout your working clone on main a second clone pinned to a release tag
Fleet file labs/multiplant/fleet.toml in the repo ~/.config/fsmes/prod/fleet.toml, mode 600, listing packs beside it
Accounts lab accounts, lab passwords the registry's accounts, passwords from ~/.config/fsmes/prod/env
Units fsmes-plant@<name> fsmes-prod-plant@<name>
Changes every merge deploy/promote.sh <tag> only

First-time setup:

git clone https://github.com/factorysemantics/factorysemantics-mes.git ~/Projects/fsmes-prod
mkdir -p ~/.config/fsmes/prod && chmod 700 ~/.config/fsmes/prod
# write fleet.toml, the packs it lists and env (see the fleet how-to); chmod 600 the file and the env
cp ~/Projects/fsmes-prod/deploy/[email protected] ~/.config/systemd/user/
systemctl --user daemon-reload
~/Projects/fsmes-prod/deploy/promote.sh v0.1.0
systemctl --user enable --now fsmes-prod-plant@<name>

promote.sh refuses a tag that is not on the remote it promotes from (FSMES_PROMOTE_REMOTE, default public), stops every plant, backs each one up — a SQLite file copied with its write-ahead log, a PostgreSQL database dumped with pg_dump -Fc — and only then checks out the tag, applies each pack and starts. It then asks each plant itself: /health must call itself by the name the fleet knows it by and /pack must say its schema is at head. Any failure after the checkout puts the previous tag, the previous venv and the backups that run made back. A plant whose database it cannot copy refuses the whole promote, and a password variable that is not set refuses it rather than creating an account with an empty password. deploy/README.md is the full list; fsmes fleet plan shows what a promote would act on.

Verify

  • curl http://127.0.0.1:<port>/health answers.
  • The log has no well-known credentials in use warning.
  • fsmes plant all status (with FSMES_PLANT_REGISTRY set) lists every plant as alive.

See also