Installation
This page is for the person who installs the gateway. It covers the appliance image, the first account, serving the gateway behind a reverse proxy, choosing modules at build time, running from source, and running the tests.
Requirements
Docker is the only requirement. The .NET SDK and Node run inside the Docker build, so nothing else needs to be installed on the host to build or run the gateway.
Running from source (see Run from source) is different: it needs the .NET SDK pinned in global.json and a NATS server.
Run the appliance
The appliance is one container that holds the gateway, a bundled NATS server with persistent JetStream, and the dashboard. From the repository root:
docker compose up --build
The API and the dashboard are both served at http://localhost:8080/. Copy .env.example to .env to change the port or the public address, or to choose modules.
What compose.yaml sets up:
| Item | Value |
|---|---|
| Image | registry.gitlab.com/scadex.io/platform/gateway:<tag>, where the tag is SCADEX_TAG (default dev) |
| Port | SCADEX_BIND:SCADEX_PORT on the host to 8080 in the container; defaults 0.0.0.0:8080 |
| Data | The named volume scadex-data, mounted at /data (DATA_DIR) |
| Drop-in modules | ./.local/modules on the host, mounted at /modules (Modules__Path) |
| Environment | AUTH_BASE_URL (default http://localhost:8080), AUTH_TRUSTED_ORIGINS (default http://localhost:8080), Modules__Disabled from SCADEX_DISABLED_MODULES |
| Restart policy | unless-stopped, with init: true |
Inside the container the entrypoint creates /data/nats/jetstream, /data/historian, /data/certificates, /data/backups, /data/internal, /data/licenses and /modules, starts nats-server on 127.0.0.1:4222 with its JetStream store under /data/nats/jetstream, then starts Scadex.Core.dll. Stopping the container stops both. NATS is a hard runtime dependency, not optional infrastructure; the appliance bundles version 2.12.3.
The runtime image is mcr.microsoft.com/dotnet/aspnet:10.0-alpine with tzdata (cron jobs keep local time in their own zone) and ICU (the SQL Server driver needs culture data). Every key the image sets is listed in Configuration.
Health check
The image declares a Docker health check: every 15 seconds, with a 3-second timeout and a 30-second start period, it fetches http://127.0.0.1:${GATEWAY_PORT}/api/v1/runtime/health and expects "status":"healthy" in the body. Anonymous callers of that route get only status; a signed-in caller gets more. Use the same route from your own monitoring.
Create the first account before exposing the gateway
The first account on a gateway is its admin, however it is created, and it is the only self-service account: after it exists, only an admin can create users. There is no setup token.
On a new install, create the first account through the loopback port before any public route to the gateway exists. Whoever reaches the empty gateway first owns it.
The Quickstart walks through the sign-up screen. For a public install, bind the container to the loopback interface while you do it (SCADEX_BIND=127.0.0.1 in .env), create the account at http://localhost:8080, and only then add the proxy route.
Serve under your own domain
Two keys tell the gateway where it lives:
| Key | Purpose |
|---|---|
AUTH_BASE_URL | The public address, for example https://gateway.example.com. An https:// value makes session cookies secure. |
AUTH_TRUSTED_ORIGINS | Comma-separated origins allowed to change anything or to open the /sync WebSocket. Empty means none, so signed-in writes get 403. Include the public origin and, if you keep using it, http://localhost:8080. |
In .env, for a gateway at https://gateway.example.com that is also reachable on the loopback port:
SCADEX_BIND=127.0.0.1
AUTH_BASE_URL=https://gateway.example.com
AUTH_TRUSTED_ORIGINS=https://gateway.example.com,http://localhost:8080
The reverse proxy in front of the gateway must:
- Terminate TLS and forward to port
8080of the container. - Pass the WebSocket upgrade for
/sync. The dashboard's live data runs over it, and the upgrade request must carry an origin listed inAUTH_TRUSTED_ORIGINS. - Not compress
text/event-streamresponses. The module log stream (/api/v1/runtime/modules/<id>/logs/stream) is server-sent events, and compression buffers it. Compressing other text responses is useful: the dashboard's cold load is several megabytes of scripts, styles and JSON.
The repository serves app.scadex.io with Traefik. The labels below are that configuration with the Scadex hostnames and host-specific routers taken out. The proxy network is the one Traefik watches, and mytls stands for your certificate resolver:
services:
gateway:
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.gateway.rule=Host(`gateway.example.com`)
- traefik.http.routers.gateway.entrypoints=websecure
- traefik.http.routers.gateway.service=gateway
- traefik.http.routers.gateway.middlewares=gateway-compress
- traefik.http.routers.gateway.tls=true
- traefik.http.routers.gateway.tls.certresolver=mytls
- traefik.http.services.gateway.loadbalancer.server.port=8080
- traefik.http.middlewares.gateway-compress.compress=true
- traefik.http.middlewares.gateway-compress.compress.excludedcontenttypes=text/event-stream
networks:
proxy:
name: proxy
external: true
Traefik passes WebSocket upgrades without extra configuration. Put this in a second compose file and layer it with COMPOSE_FILE=compose.yaml:your-file.yaml in .env, as the repository does with infra/scadex.io/compose.yaml.
Choose modules at build time
Modules are optional. SCADEX_MODULES picks what goes into the image:
docker compose build # all modules (the default)
SCADEX_MODULES="mqtt opcua" docker compose build # only these folder names under modules/
SCADEX_MODULES="" docker compose build # core only
The names are folders under modules/ in the repository: cron, historian, mqtt, opcua. The build stages each one with scripts/stage-modules.sh into /app/modules of the image.
A module can also be added later without rebuilding. Each subfolder of the /modules mount (./.local/modules on the host) is one installed module, and one with the same id as a built-in module replaces it, including its stored connections and credentials; treat write access to that folder as admin access. To stage a module from the repository into the mount with the SDK container:
mkdir -p .local/modules
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/src" -w /src \
mcr.microsoft.com/dotnet/sdk:10.0.403 scripts/stage-modules.sh Release .local/modules mqtt
docker compose restart gateway
A module can also be uploaded as a zip while the gateway runs, from Gateway > System > Modules or the API, with no restart. SCADEX_DISABLED_MODULES (Modules__Disabled) lists installed module ids the gateway must not load. See Modules for the lifecycle and Tools for the staging script.
Run from source
Gateway development runs the gateway on the host with hot reload. It needs the .NET SDK version pinned in global.json (10.0.403, with rollForward: latestPatch) and a JetStream-enabled NATS server:
docker run --rm -p 4222:4222 nats:2.12.3-alpine -js
dotnet build Scadex.slnx
scripts/stage-modules.sh
AUTH_TRUSTED_ORIGINS=http://localhost:3000 dotnet watch --project src/Scadex.Core/Scadex.Core.csproj
Without the container's environment the defaults apply: the gateway listens on port 3000, AUTH_BASE_URL is http://localhost:3000, DATA_DIR is data under the content root, and scripts/stage-modules.sh with no arguments publishes every module in Debug configuration into src/Scadex.Core/bin/Debug/net10.0/modules, which is the default Modules__Path. AUTH_TRUSTED_ORIGINS has no default, so set it or every write gets 403.
The dashboard is not built in this mode. Either build it once (pnpm install && pnpm turbo build, which writes into src/Scadex.Core/wwwroot) or run it with Vite as below.
Dashboard development
compose.dev.yaml runs a containerised gateway and a Vite dev server with hot reload:
docker compose -f compose.dev.yaml up --build
| Service | Host port | Notes |
|---|---|---|
dashboard | 5173 (SCADEX_DEV_DASHBOARD_PORT) | Built from deploy/Dockerfile.dev; apps/dashboard/src, packages/sdk/src and packages/ui/src are bind-mounted, node_modules stays in the container |
gateway | 3000 (SCADEX_DEV_GATEWAY_PORT) | GATEWAY_PORT=3000, AUTH_BASE_URL=http://localhost:3000, AUTH_TRUSTED_ORIGINS=http://localhost:5173, data in the gateway-data volume |
The Vite proxy sends API calls to the gateway hostname on port 3000. After a dependency change, rebuild the dashboard image.
To check the frontend the way CI does:
pnpm install
pnpm turbo build lint test
Run the tests
The .NET tests, including the module boundary rules, run as a Docker build target so no SDK is needed on the host:
docker build -f deploy/Dockerfile --target test .
The end-to-end tests start a real MQTT broker and OPC UA server in containers and need Node on the host:
tests/integration/run.sh
Next
- Quickstart: the first account, a first tag, a first connection and a first screen.
- Configuration: every key, the data directory layout, what needs a restart.
- Upgrading: pulling a new image and what happens on first start.