Skip to content

Installation

Learning objectives

After reading this page you will understand:

  • Which of the five installation modes fits what you are trying to do
  • How the container deployment is wired, and why the web GUIs reach the exchange without any published port being involved
  • What 127.0.0.1 and 0.0.0.0 mean in each of the three separate places they appear, which of them actually protects you, and how the core plane and the service layer come by their different defaults
  • Every flag that controls a build or a start, and every directory the system reads or writes

New to EduMatcher? The Quick Start's Install and Start is the short version of this chapter: one route, step by step. This chapter is the complete reference. How a release is produced is described in the Architecture and Developer Guide's The Release Process.

Choosing an installation mode

The first question is whether you want to run EduMatcher or change it.

Mode Best for Needs on your machine What you get Command style
Containers, one command Running a venue: classroom, demo, self-study Podman or Docker The exchange and all five web GUIs ./edumatcher.sh start
Containers from source Changing the code and seeing it run as a system Podman/Docker, Poetry, Node The same, built from your checkout make up-all
VM bootstrap Workshops where host setup must stay untouched Multipass, curl Backend inside a Multipass VM multipass shell ems
pipx Students running processes by hand, one per terminal Python 3.13, pipx pm-* commands on your PATH pm-engine
Poetry checkout Developing, running the test suite Python 3.13, Poetry Repository plus dev dependencies poetry run pm-engine

The container modes give you the whole system — engine, gateways, REST API and the five browser applications — with one command. The pipx and Poetry modes give you the individual processes to start yourself, which is what the rest of this guide assumes when it shows a bare pm-* command. In Poetry mode, prefix those commands with poetry run.

If you are not sure

Use the one-command container install. It is the fastest route to a running market, and nothing it installs is outside a single directory you can delete.

Containers: the whole system in one command

Everything this install creates lives in one directory, ~/.edumatcher by default. Nothing is written anywhere else — no system paths, no service registered, no change to your PATH — so removing that directory removes the installation.

Inside it Holds
~/.edumatcher/data Every trade, order book, log and database the exchange produces. Mounted into the containers, so it is on your disk rather than inside one, and survives stop, start and update
~/.edumatcher/config An engine configuration of your own, when you supply one
~/.edumatcher/compose.yaml, .env, edumatcher.sh The deployment itself

Pick a different location with --dir; the layout is the same underneath it. Two Compose volumes sit outside that directory, in your container engine's storage — the Log Operator Console's acknowledgements and TapeDeck's failover log. Neither holds exchange data, and ./edumatcher.sh uninstall removes both.

~/.edumatcher/data is the directory to back up, and the one to delete

./edumatcher.sh uninstall keeps it; uninstall --data is what erases a venue's history. ./edumatcher.sh mounts shows which directory is behind each path inside each container if you are ever unsure.

With that established:

curl -fsSL https://raw.githubusercontent.com/johan162/EduMatcher/main/deployment/curl/install.sh | bash

Then open http://localhost:8090.

That starts a complete exchange plus five web applications, from images built and published by the project. Nothing is compiled on your machine: no Python, no Node, no checkout. The only requirement is Podman or Docker.

Application URL What it is
TapeDeck market display (pm-terminal) http://localhost:8090 Live order books, trades and market data
Log Operator Console (pm-log-ui) http://localhost:8091 The centralized log, searchable, with acknowledgements
Configuration GUI (config-gui) http://localhost:8092 Author an engine_config.yaml in the browser
Trading GUI (pm-trading-ui) http://localhost:8093 Submit and manage orders as a trading participant
Order Book Viewer (pm-book) http://localhost:8094 One symbol's full order book, session statistics and trade tape — the browser companion to pm-viewer
REST API docs http://localhost:8080/docs Swagger UI for the desk API gateway

What the installer does

  1. Checks that Podman or Docker is present, preferring Podman when both are.
  2. Resolves the newest release (or the one you named with --version).
  3. Downloads compose.yaml, compose.zmq.yaml, edumatcher.sh and .env.example from that release's tag, so the files and the images always come from one commit.
  4. Writes .env, creating data/ and config/ beside it.
  5. Pulls the six images and starts them.

Step 3 is worth noting: the support files come from the release tag being installed, not from a branch, so the compose file and the images can never describe different systems.

Installer options

Option Default Effect
--version X.Y.Z newest release Install one specific release. All six images carry this tag, so it pins the whole system
--config NAME s10-basic Deploy a bundled example configuration
--config FILE — ...or a path to an engine_config.yaml of your own
--dir PATH ~/.edumatcher Where to install
--no-start — Fetch and configure, but do not start anything
--help — Show the options

Because the script is read from a pipe, options need bash -s -- so that the shell hands them to the script rather than consuming them itself:

curl -fsSL .../install.sh | bash -s -- --config s10-nominal --version 0.50.4

Two environment variables are also honoured: REPO_OWNER (which GitHub account and GHCR namespace to use) and REPO_REF (fetch the support files from a branch instead of the release tag, for testing an unreleased installer).

Configuring it: the .env file

~/.edumatcher/.env is where every choice about a running install lives. The installer writes it from .env.example on the first run and keeps your copy afterwards, so a later ./edumatcher.sh update re-pins the version without discarding anything else you changed.

Compose reads that file automatically because it sits beside compose.yaml. Nothing else does: there is no separate configuration for the containers, and the compose file itself contains no settings, only ${VARIABLE} references into this file.

Variable Default What it does
EM_VERSION (the installed release) Which release to run. All six images carry this tag, so one value pins the whole system. latest follows the newest release
GHCR_OWNER johan162 The GHCR namespace the images are pulled from. Change it only for a fork
EM_CONFIG s10-basic Which bundled example configuration the exchange deploys
EM_CONFIG_FILE (empty) Set to /config/engine_config.yaml when you run a configuration of your own. Non-empty wins over EM_CONFIG
EM_PROFILE mm-demo Which processes start: mm-demo (the full stack plus a slow, passive market-maker bot quoting as MM01), ai-swarm (mm-demo with its market maker quoting around the market model's values, plus the model and twenty AI traders), default, mini or micro. See Processes, Environment and Ports
TZ UTC Container timezone. Set it to match the trading calendar in your configuration, e.g. Europe/Stockholm
BIND_ADDR 127.0.0.1 Which host interface the published ports listen on. See the warning below
EDUMATCHER_GATEWAY_BIND_HOST 0.0.0.0 Bind host for the service-layer listeners inside the container — the four protocol gateways, pm-log-srv and pm-api-gwy. It is what makes them reachable from the GUI containers, and it wins over any bind_address: in the deployed configuration. Not a host-exposure setting; that is BIND_ADDR
EM_ZMQ 0 1 also publishes the raw ZeroMQ bus (5555-5559, 5601/5602) and tells the engine and pm-index to bind the container interface, so tools on your machine can attach. The equivalent of make up ZMQ=1
TERMINAL_GUI_PORT 8090 Host port for TapeDeck (pm-terminal)
LOG_GUI_PORT 8091 Host port for the Log Operator Console (pm-log-ui)
CONFIG_GUI_PORT 8092 Host port for the configuration builder
TRADER_GUI_PORT 8093 Host port for the trader GUI
BOOK_GUI_PORT 8094 Host port for the order book viewer

Two settings have their own commands, because editing them by hand is easy to get half-right — EM_CONFIG and EM_CONFIG_FILE must agree, and EM_VERSION needs an image pull to take effect:

./edumatcher.sh config s10-nominal    # sets EM_CONFIG, clears EM_CONFIG_FILE
./edumatcher.sh config ./mine.yaml    # copies the file, sets EM_CONFIG_FILE
./edumatcher.sh update 0.26.2         # sets EM_VERSION, pulls, restarts

Everything else is a plain edit followed by ./edumatcher.sh restart.

BIND_ADDR=0.0.0.0 puts an unauthenticated exchange on your network

It is the right setting for a classroom where students connect to the instructor's machine, and the wrong one on a network you do not control: the protocol gateways have no password. The default keeps everything on this machine.

Values you will not find in .env

TapeDeck needs API_GATEWAY_URL and PM_TERMINAL_API_KEY, and the order book viewer API_GATEWAY_URL and PM_BOOK_API_KEY — the same read-only key. Those are resolved at startup and injected, not stored: the read-only API key is generated per engine configuration — a different one in each bundled example — and lives on a different gateway instance than the trading credentials. ./edumatcher.sh start reads the deployed configuration and passes them to both applications. Setting them in .env would only go stale the next time you switched configuration.

Everyday commands

Everything is driven by one script in the install directory:

cd ~/.edumatcher
./edumatcher.sh status              # containers, plus the exchange process table
./edumatcher.sh logs terminal-gui   # follow one service
./edumatcher.sh shell               # a shell inside the exchange container
./edumatcher.sh shell pm-config-show      # ...or one command in it
./edumatcher.sh urls                # the application table, with your ports
./edumatcher.sh mounts              # which directory is behind each container path
./edumatcher.sh stop                # stop everything; ./data is kept
./edumatcher.sh start               # bring it back
./edumatcher.sh update              # pull the newest release and restart
./edumatcher.sh update 0.20.5       # ...or move to an exact one
./edumatcher.sh uninstall           # remove containers and volumes, keep data
./edumatcher.sh uninstall --data    # remove everything

Picking up a new web application

update pulls new images for the files you already have; it does not re-download compose.yaml. When a release adds an application — the order book viewer on port 8094 arrived this way — re-run the one-line installer instead. It keeps your .env and data/, and fetches the release's compose.yaml and edumatcher.sh.

Choosing what the exchange trades

EduMatcher ships 30 ready-made configurations: 1, 3, 10, 30 or 150 order books, each as a basic, nominal or complex variant, and each of those with or without seeded market-maker quotes. They are inside the backend image, so no download is involved in switching.

./edumatcher.sh config s10-nominal
./edumatcher.sh restart

The names are s1, s3, s10, s30 and s150 combined with basic, nominal and complex, each with an optional -nomm suffix for the variant with empty order books instead of seeded market-maker quotes (for example s3-basic-nomm). The s150-* configs also seed AI001-AI020 for a 20-bot pm-ai-swarm run unchanged. ./edumatcher.sh config lists them all if you mistype one. See Example Engine Configs for what each contains.

To run a configuration of your own, give the same command a path instead:

./edumatcher.sh config ./my-market.yaml
./edumatcher.sh restart

The file is copied into ~/.edumatcher/config/, mounted read-only into the container, and deployed on every start — so editing it and restarting is the whole edit-test loop. Switching back to a bundled example is ./edumatcher.sh config s10-basic. The configuration builder at http://localhost:8092 is the easy way to author one; see Configuration GUI.

Containers from the source checkout

Use this when you are changing EduMatcher and want to see your change running as a whole system. It builds the same six images from your working tree.

git clone https://github.com/johan162/EduMatcher.git
cd EduMatcher/deployment/docker
make build          # builds a wheel from the checkout, then the image
make up-all         # exchange + terminal-, log-, trader- and book-gui

make help lists every target. The two that matter are up (the exchange alone) and up-all (the exchange plus the GUIs).

Flags that control the build

make build decides where the EduMatcher package comes from:

Flag Result
(none) Build a wheel from this checkout with Poetry and install that. This is the default deliberately: a rebuild after editing source must pick up the edit
PYPI=1 Ignore the checkout; install the newest release from PyPI
VERSION=x.y.z Install exactly that PyPI release (implies PYPI=1)
DEV=1 Accepted and ignored — it used to select the local wheel, which is now the default
CONTAINER_ENGINE=docker Force Docker when Podman is also installed

Confirm which package went in

A successful build prints a line of the form Installing local wheel: /tmp/wheel/edumatcher-<version>-<build-tag>.whl (the exact filename comes from whatever Poetry built, so it may include a platform or Python-tag suffix beyond the version). If instead you see pip downloading edumatcher, the image is running a published release rather than your code — and a source change you are hunting for will appear not to take effect.

Flags that control a start

Flag Default Effect
CONFIG=<name> s10-basic Deploy a bundled example
CONFIG=<file> — Deploy an engine_config.yaml of your own; the file is copied to deployment/docker/config/ and mounted read-only
PROFILE=<name> default Which processes pm-opctl-cli starts: default, mini, micro, mm-demo or ai-swarm. See Processes, Environment and Ports
ZMQ=1 off Also publish the raw ZeroMQ bus (5555-5559, 5601-5602) to the host, and set the engine and index sockets to bind 0.0.0.0 inside the container
SSH=1 off Run sshd in the container on SSH_PORT, authorised by your ~/.ssh/*.pub
CONFIG_GUI=1 off Include the configuration builder in up-all. It is opt-in because it talks to nothing — it is a standalone authoring tool

Flags combine: make up-all CONFIG=s10-complex PROFILE=mini ZMQ=1 CONFIG_GUI=1.

Settings in .env

deployment/docker/.env is created from .env.example on the first build. Compose reads it automatically because it sits beside the compose files, and it is git-ignored — so it is the right place for choices specific to your machine, as opposed to a make flag, which applies to one invocation only.

When the same setting is available in more than one place, the nearer one wins:

make flag  →  shell environment  →  .env  →  compose ${VAR:-default}  →  image ENV

That is why make up-all CONFIG=s10-nominal does not edit .env, and why a plain make up-all afterwards goes back to whatever .env says.

Variable Default Meaning
COMPOSE_PROJECT_NAME edumatcher Compose project name; decides container and network naming
EDUMATCHER_VERSION (empty) PyPI version for PYPI=1 builds; empty means newest
WITH_SSH 1 Install openssh-server into the image at build time
EM_CONFIG s10-basic Bundled example to deploy
EM_CONFIG_FILE (empty) Path inside the container to a configuration of your own; set for you by CONFIG=<file>
EM_PROFILE default Process profile to start
TZ Europe/Stockholm Container timezone — match the trading calendar in your configuration
BIND_ADDR 127.0.0.1 Which host interface the published ports listen on. See below
EDUMATCHER_GATEWAY_BIND_HOST 0.0.0.0 Bind host for the service-layer listeners inside the container — the four protocol gateways, pm-log-srv and pm-api-gwy. It is what makes them reachable from the GUI containers, and it wins over any bind_address: in the deployed configuration. Not a host-exposure setting; that is BIND_ADDR
SSH_PORT 2222 Host port forwarded to sshd
TERMINAL_GUI_PORT 8090 Host port for TapeDeck (pm-terminal)
LOG_GUI_PORT 8091 Host port for the Log Operator Console (pm-log-ui)
CONFIG_GUI_PORT 8092 Host port for the configuration builder
TRADER_GUI_PORT 8093 Host port for the trader GUI
BOOK_GUI_PORT 8094 Host port for the order book viewer
IMAGE_NAME / IMAGE_TAG edumatcher / local Image naming
CONTAINER_NAME edumatcher Container name

Beyond these, the compose files read a few variables that have sensible defaults and no .env entry: CORS_ORIGIN, MAX_WS_CLIENTS, CALF_CLIENT_ID and INDEX_IDS for TapeDeck, LOG_SRV_ENABLED for its logging uplink, and PIP_INDEX_URL as a build argument. Export any of them in your shell before make up-all if you need to.

API_GATEWAY_URL and PM_TERMINAL_API_KEY are read the same way but should be left alone: up-all resolves them from the deployed configuration and injects them, for the reasons in The read-only API key.

Image build arguments

Passed with --build-arg, or through the compose files' args: blocks. You need these mainly behind a corporate proxy.

The backend image (deployment/docker/Dockerfile):

Argument Default Purpose
PYTHON_VERSION 3.13 Base image tag for both build and runtime stages
EDUMATCHER_VERSION (empty) PyPI version to install when no local wheel is present
PIP_INDEX_URL (empty) Point pip at an internal index such as Artifactory
WITH_SSH 1 Set to 0 for a smaller image with no openssh-server

Each web GUI image (web-apps/*/Dockerfile):

Argument Default Purpose
WEB_PORT per app Port the server listens on inside the container
NPM_REGISTRY https://registry.npmjs.org/ Internal npm mirror. The Dockerfiles accept this argument, but the compose files' args: blocks do not currently pass it through, so setting it in your shell has no effect via make up-all/make build-guis — pass --build-arg NPM_REGISTRY=... directly to podman build/docker build instead
NPM_STRICT_SSL true Set false when a proxy re-signs TLS
HTTP_PROXY / HTTPS_PROXY / NO_PROXY (empty) Honoured by npm during the build
USE_PROXY_CA / CA_CERT_FILE (empty) Declared only by the unused web-apps/config-gui/Dockerfile.proxy, not the Dockerfile that compose.config-gui.yaml actually builds — setting these currently has no effect on make up-all CONFIG_GUI=1

How the containers are wired

This section is worth reading even if the install "just worked", because the one thing that reliably confuses people here — the difference between 127.0.0.1 and 0.0.0.0 — appears in three separate places that mean three different things, and only one of them is a security boundary.

One compose project, one private network

All six containers belong to a single Compose project, so Compose puts them on one private network and gives each a name on it. The backend answers to the hostname edumatcher.

flowchart LR
    subgraph host["Your machine"]
        BROWSER["Browser"]
        CLIENT["curl, Swagger,\nprotocol clients"]
    end

    subgraph project["Compose project — one private network"]
        TERM["terminal-gui\n:8090"]
        LOG["log-gui\n:8091"]
        CFG["config-gui\n:8092"]
        TRD["trader-gui\n:8093"]
        BOOK["book-gui\n:8094"]
        BE["edumatcher\nengine + all gateways"]
    end

    BROWSER -->|"published 8090-8094\nvia BIND_ADDR"| TERM
    BROWSER --> LOG
    BROWSER --> CFG
    BROWSER --> TRD
    BROWSER --> BOOK
    CLIENT -->|"published 5560, 5565, 5570,\n5580, 5590, 5600, 8080-8081\nvia BIND_ADDR"| BE

    TERM -->|"edumatcher:5570 market data\nedumatcher:8081 history"| BE
    LOG -->|"edumatcher:5601 / :5602 log stream"| BE
    TRD -->|"edumatcher:8080 REST + WebSocket"| BE
    BOOK -->|"edumatcher:8081 market-data WebSocket\n+ history"| BE

Two things follow, and both are easy to get wrong:

  • The GUIs do not use the published ports. Their traffic never leaves the private network. The ports published on your machine exist for you — your browser, curl, Swagger, the protocol example clients.
  • Nothing needs host.docker.internal. Older setups pointed the GUIs at the host and back in again, which behaved differently on Docker Desktop, Linux Docker and Podman. Sharing one project removes the problem rather than working around it.

The configuration builder is the exception: it talks to nothing at all, which is why it is opt-in with CONFIG_GUI=1 when building from source.

The two planes, and where each address is decided

Every listening socket in EduMatcher belongs to one of two planes, and the two have deliberately different defaults.

flowchart TB
    subgraph ns["Inside the backend container's network namespace"]
        subgraph core["Core plane — ports 5555-5559"]
            ENG["pm-engine\n5555 pull · 5556 pub · 5557 drop-copy"]
            IDX["pm-index\n5558 pub · 5559 pull"]
        end
        subgraph svc["Service layer — every other listener"]
            GW["pm-alf-gwy 5565 · pm-md-gwy 5570\npm-balf-gwy 5560 · pm-ralf-gwy 5580\npm-dc-gwy 5590 · pm-log-srv 5600/5601/5602\npm-api-gwy 8080/8081"]
        end
    end
    core -->|"defaults to 127.0.0.1"| N1["Private to the container.\nOpen it with EDUMATCHER_ENGINE_BIND_HOST\nand EDUMATCHER_INDEX_BIND_HOST — ZMQ=1 does this"]
    svc -->|"defaults to 0.0.0.0"| N2["Reachable from sibling containers\nand through published ports.\nNarrow it with EDUMATCHER_GATEWAY_BIND_HOST"]

The core plane is the internal bus. pm-engine and pm-index speak ZeroMQ to the other EduMatcher processes and to nothing else, so they default to 127.0.0.1 and a stock install keeps the bus inside the container. Starting with ZMQ=1 sets EDUMATCHER_ENGINE_BIND_HOST and EDUMATCHER_INDEX_BIND_HOST to 0.0.0.0 and publishes 5555–5559, so you can attach your own ZeroMQ client from the host.

The service layer is what clients talk to. All seven gateway processes default to 0.0.0.0, because every practical deployment needs them reachable from somewhere else — a sibling GUI container, a student's laptop in a classroom, a protocol example running on your host.

Each service listener resolves its bind host in this order, first match wins:

Precedence Source Example
1 The process's own command-line flag — --bind on the four protocol gateways, --host on pm-log-srv and pm-api-gwy pm-md-gwy --bind 127.0.0.1
2 The EDUMATCHER_GATEWAY_BIND_HOST environment variable EDUMATCHER_GATEWAY_BIND_HOST=127.0.0.1
3 bind_address: (or host: for pm-api-gwy) in the engine configuration bind_address: 10.0.0.5
4 The built-in default 0.0.0.0

EDUMATCHER_GATEWAY_BIND_HOST deliberately sits above the configuration file. It is a deployment-wide switch: one environment variable closes every service listener at once, without editing a configuration that may not be yours to edit.

What 0.0.0.0 does and does not expose

Where Set by 127.0.0.1 means 0.0.0.0 means
Published ports — the host side BIND_ADDR in .env Only this machine can reach the exchange Anyone on your network can
Service listeners — inside the container default, or EDUMATCHER_GATEWAY_BIND_HOST, or bind_address: / host: Only processes in that container — not even a sibling container Any container on the Compose network, and anything reaching a published port
Core plane — inside the container default, or EDUMATCHER_ENGINE_BIND_HOST / EDUMATCHER_INDEX_BIND_HOST; set by ZMQ=1 Same: container-internal only Reachable from the Compose network and publishable

Only the first row is a security boundary. The second and third are inside a network namespace that already isolates the container: binding 0.0.0.0 there exposes nothing to the outside world, because there is no route in except through a port you chose to publish. Whether the exchange is visible on your LAN is decided entirely by BIND_ADDR.

BIND_ADDR=0.0.0.0 puts an unauthenticated exchange on your network

It is the right setting for a classroom where students connect to the instructor's machine. It is the wrong setting on a network you do not control. There is no password on the order-entry gateways — anyone who can open the socket can submit orders.

On bare metal there is no namespace

A pipx or Poetry install has no container around it, so a service listener on 0.0.0.0 really is on your network. If that is not what you want, export EDUMATCHER_GATEWAY_BIND_HOST=127.0.0.1 — one variable, every gateway, no configuration edits. This is a change in default behaviour; see the note in CHANGELOG.md.

Why there is no longer a loopback rewrite

Earlier versions shipped the bundled examples with bind_address: 127.0.0.1 and had the container entrypoint rewrite them to 0.0.0.0 on the way in. That rewrite worked, but it meant the deployed configuration was not the configuration you wrote, and pm-config-show described one thing while the processes did another.

The gateways now bind 0.0.0.0 themselves, every bundled example says 0.0.0.0, and the entrypoint rewrites nothing: the deployed configuration is the configuration you supplied, and make config-show describes what the processes actually do.

Two things that look like evidence and are not

Both of these will tell you a gateway is reachable when it is not:

"I can reach the port from the host." Podman's rootless port forwarding delivers a published connection to loopback inside the container's namespace. A listener bound to 127.0.0.1 therefore answers host traffic perfectly, while a sibling container connecting across the private network gets ECONNREFUSED. Host reachability says nothing about container-to-container reachability. The stock defaults avoid this, but you can still walk into it by setting EDUMATCHER_GATEWAY_BIND_HOST=127.0.0.1, by passing --host to a process, or by supplying a configuration that pins bind_address: to loopback.

"make status is all green." pm-opctl-cli list shows tcp connect to 127.0.0.1:5570 ok, but that check also runs inside the container, over the same loopback. A healthy process table is not evidence that anything outside that container can connect.

The check that settles it is the compiled configuration, which is the file the processes actually read:

podman exec edumatcher python3 -c \
  "import json; print(json.load(open('/data/ref_data/engine_config.json'))['market_data_gateway'])"

and, from a GUI container, an actual connection attempt:

podman exec edumatcher-terminal-gui node -e '
const net = require("net");
const s = net.connect(5570, "edumatcher", () => { console.log("OPEN"); s.end(); });
s.on("error", e => console.log("FAIL", e.code));'

The read-only API key

One value cannot be a fixed default. TapeDeck reads historical data, and the order book viewer reads everything, through pm-api-gwy using the credential whose gateway_id is null. That key is generated per engine configuration — every bundled example has a different one — and it is issued on the dashboards gateway instance (port 8081), not the desk instance (8080) that carries the trading credentials.

Both start paths therefore run in two phases: bring up the exchange, read the key out of the deployed configuration, then start the GUIs with it. If a configuration has no such credential the start still succeeds and says so — the TapeDeck's live market-data feed needs no key, only its history panels do; the order book viewer, which reads all its data through pm-api-gwy, then has nothing to show.

Directories and paths

Every location the system reads or writes, and what controls it.

The data directory

One variable decides where an exchange keeps its state:

Path Controlled by Contains
<DATA_DIR> EDUMATCHER_DATA_DIR; see Environment variables in the Reference Manual for how the default is chosen Everything below
<DATA_DIR>/ref_data/engine_config.json pm-setup, pm-config-deploy The compiled artifact every process reads
<DATA_DIR>/ref_data/engine_config.yaml same The authored source it was compiled from, kept for provenance
<DATA_DIR>/emo/ pm-opctl-cli One log per process, PID files, and the active profile name
<DATA_DIR>/log.db pm-log-srv The centralized log
<DATA_DIR>/stats.db pm-stats Statistics and history behind the REST API
<DATA_DIR>/clearing.db pm-clearing Positions and P&L
<DATA_DIR>/audit.log, audit_index.db pm-audit The audit trail and its index
<DATA_DIR>/gtc_orders.json, gtc_combos.json, book_stats.json pm-engine Resting orders and book state across restarts
<DATA_DIR>/logs/ log clients Failover logs written when pm-log-srv is unreachable

Configured relative paths such as data/stats.db resolve under <DATA_DIR>, so they mean the same file no matter which directory a process was started from. Absolute paths in the configuration remain explicit overrides.

Where that lands in each mode

Mode <DATA_DIR> is Controlled by
One-command containers ~/.edumatcher/data on your disk, mounted at /data in the container --dir at install time
Containers from source deployment/docker/data, mounted at /data The ./data:/data mount in compose.yaml
pipx ~/.local/share/edumatcher EDUMATCHER_DATA_DIR
Poetry checkout <repo>/src/data/ EDUMATCHER_DATA_DIR
VM /home/ubuntu/.local/share/edumatcher inside the VM (same installed default as pipx) mknode.sh, via EDUMATCHER_DATA_DIR in .bashrc

The container images set EDUMATCHER_DATA_DIR=/data internally, so the container's view and your directory are the same files. Your data is on your disk, not inside a container: it survives stop, start, rebuild and update.

Supporting directories

Path Created by Purpose
~/.edumatcher/ the installer (--dir to change) compose.yaml, .env, edumatcher.sh, data/, config/
~/.edumatcher/config/ ./edumatcher.sh config <file> Your own configuration, mounted read-only at /config
deployment/docker/.env first make build or make up Machine-specific settings; git-ignored
deployment/docker/config/ make up-all CONFIG=<file> As above, for the source-built stack
deployment/docker/.wheel/ make build The locally built wheel the image installs from
deployment/docker/.ssh/ make keys authorized_keys assembled from your ~/.ssh/*.pub, for SSH=1
/backend-data (in log-gui) the ./data:/backend-data:ro mount Where the Log Operator Console reads log.db — read-only by construction, so it can never write the log server's database
log-gui-acks, terminal-gui-logs, book-gui-logs Compose named volumes Log acknowledgements; the terminal's and the order book viewer's failover logs

VM bootstrap — a ready-to-run Multipass VM

Use this when you want the fewest assumptions about your host machine. Your host needs Multipass and curl; Python and EduMatcher are installed inside the VM. This mode installs the backend only — the web GUIs are part of the container deployment. Provisioning runs pm-setup, so the VM comes up with a deployed configuration and pm-opctl-cli ready to start the stack.

curl -fsSL https://raw.githubusercontent.com/johan162/EduMatcher/main/deployment/vm/curl_setup_vm.sh | \
    bash -s -- --version 0.50.4 --snapshot

multipass shell ems
cd /home/ubuntu/session
pm-opctl-cli start
Option Default Purpose
--name <vm> ems Name the VM
--version X.Y.Z dev Install a specific EduMatcher release. The default installs a local wheel, so pass this unless you have a checkout
--dev — Install the wheel from the repository's dist/ instead of PyPI
--cpus / --memory / --disk 4 / 4G / 6G Size the VM
--image <name> lts Base Multipass image
--snapshot-name <name> clean Name of the snapshot taken after provisioning
--snapshot already on Snapshots are taken by default; the flag only makes that explicit
--ssh-key <path> ~/.ssh/<vm>_ed25519 Private key whose public half is installed for passwordless login

To read the script before running it:

curl -fsSL https://raw.githubusercontent.com/johan162/EduMatcher/main/deployment/vm/curl_setup_vm.sh -o curl_setup_vm.sh
less curl_setup_vm.sh
bash curl_setup_vm.sh --version 0.50.4 --snapshot

pipx — commands on your own machine

Use this when you want to run the processes yourself, one per terminal, which is how most of this guide is written.

Requirement Notes
Python 3.13 or later Check with python --version
pipx Installs command-line applications into isolated environments
Several terminals Or tmux / screen; one process per pane is normal
# macOS with Homebrew
brew install pipx
pipx ensurepath

# Linux / generic Python install
python -m pip install --user pipx
python -m pipx ensurepath

Then install and bootstrap a session directory:

pipx install edumatcher
mkdir edumatcher-session
cd edumatcher-session
pm-setup

pm-setup creates the data directory, deploys a bundled example configuration and prints the EDUMATCHER_DATA_DIR line to add to your shell profile. Open a new terminal afterwards so every pm-* command sees the same data directory.

pm-setup --config s10-nominal   # a different bundled example
pm-setup --data-dir ~/my-venue  # an explicit location
pm-setup --force                # replace an already-deployed configuration
pm-setup --no-config            # create the directory only

Poetry checkout — developing EduMatcher

git clone https://github.com/johan162/EduMatcher.git
cd EduMatcher
poetry config virtualenvs.in-project true
poetry install --with dev,docs

poetry run pm-engine --verbose
poetry run pm-alf-console --id TRADER01

Developer mode uses the repository-local data directory. When exact behaviour matters, use the same deployed-configuration flow as installed mode: author YAML, run poetry run pm-config-deploy ..., then restart the processes.

Shell completion

Every pm-* command completes its subcommands, options and choices= values in bash and zsh — pm-audit-cli ev<Tab> completes to events, pm-engine --log-level <Tab> lists CRITICAL ERROR WARNING INFO DEBUG, and so on. pm-help --completion bash|zsh prints the script; nothing needs to be generated by hand.

Container: nothing to do — the backend image builds completion in at image-build time and every interactive make shell / make ssh session already has it.

pipx or pip install into a venv: add one line to your shell profile —

# ~/.bashrc
command -v pm-help >/dev/null && eval "$(pm-help --completion bash)"
# ~/.zshrc, after compinit
(( $+commands[pm-help] )) && eval "$($commands[pm-help] --completion zsh)"

Poetry checkout: the venv is usually not active when your shell profile runs, so point at it by its absolute path —

# ~/.bashrc
EM=/path/to/EduMatcher/.venv/bin/pm-help
[ -x "$EM" ] && eval "$("$EM" --completion bash)"
# ~/.zshrc, after compinit
EM=/path/to/EduMatcher/.venv/bin/pm-help
[[ -x $EM ]] && eval "$($EM --completion zsh)"

An editable install (poetry install) reads the completion scripts straight from the checkout, so they follow whichever branch is checked out. Calling pm-help at every shell start (rather than sourcing a saved copy) keeps completion matched to the installed version, at the cost of pm-help's own ~0.2s start-up per new shell. To avoid that, cache it once per upgrade instead:

pm-help --completion bash > ~/.local/share/edumatcher/pm-completion.bash
# ~/.bashrc:  source ~/.local/share/edumatcher/pm-completion.bash
pm-help --completion zsh > ~/.local/share/edumatcher/pm-completion.zsh
# ~/.zshrc:   eval "$(<~/.local/share/edumatcher/pm-completion.zsh)"

zsh's own completion system (compinit) must already be initialised — the snippets above assume the usual autoload -Uz compinit && compinit earlier in ~/.zshrc. Loading the file with source instead of eval silently registers nothing; always use eval for the zsh script.

Where to go next