Railway deployment
Experimental cloud path. Open-FDD is local-first and currently intended for LAN/VPN/OT networks. The Railway recipes below are for labs, demos, and controlled evaluation. They are not a claim that Open-FDD is production-hardened for direct public-internet exposure.
Open-FDD publishes a Rust/React container stack to GHCR. Railway can run the cloud-friendly subset directly from those prebuilt images without rebuilding the application.
Operator checklist: RAILWAY_DEPLOYMENT_CHECKLIST.md.
Deployment sequencing (required)
Railway does not guarantee peer service DNS or health before dependents start. Agents and humans must:
- Deploy
openfdd-centraland wait untilGET /api/healthreturns 200. - Deploy
openfdd-mqttwhen this is a cloud MQTTS hub (default for live OT). - Deploy
openfdd-weblast, withOPENFDD_CENTRAL_UPSTREAMpointing at central’s private DNS.
Healthcheck windows on Railway are often ~30s. If web starts while central’s .railway.internal name is not yet resolvable, old web images failed nginx startup with host not found in upstream. Tip openfdd-web images use lazy DNS (nginx resolver + variable proxy_pass) so the process can boot and resolve central on the first /api request. Still deploy central first — lazy DNS does not invent a healthy peer.
What to deploy
A. Cloud MQTTS hub (preferred for live OT)
| Railway service | Image | Container port | Exposure |
|---|---|---|---|
openfdd-central |
ghcr.io/bbartling/openfdd-central:nightly |
8080 |
Private |
openfdd-mqtt |
ghcr.io/bbartling/openfdd-mqtt:nightly |
8883 |
Private MQTTS |
openfdd-web |
ghcr.io/bbartling/openfdd-web:nightly |
8080 |
Public HTTP |
Why mqtt is on by default for this path: the cloud service’s job is to host MQTTS so on-prem openfdd-fieldbus can publish securely into Central. CSV-only labs may omit mqtt; live OT should not.
Keep fieldbus on-prem (OT LAN / VPN). Do not expect BACnet discovery inside Railway.
B. Minimal CSV / package lab
| Railway service | Image | Container port | Exposure |
|---|---|---|---|
openfdd-central |
ghcr.io/bbartling/openfdd-central:nightly |
8080 |
Private preferred |
openfdd-web |
ghcr.io/bbartling/openfdd-web:nightly |
8080 |
Public HTTP |
The browser talks to the React web service; the web image proxies same-origin /api and /twins to central.
Use :nightly for the latest green master channel or pin :sha-<7> for a reproducible deployment. Do not invent semver tags that are not published.
Optional
| Service | Image | Cloud notes |
|---|---|---|
| MCP | ghcr.io/bbartling/openfdd-mcp:nightly |
Optional agent sidecar. Set OPENFDD_API_BASE to central. |
| Fieldbus | ghcr.io/bbartling/openfdd-fieldbus:nightly |
Run on-prem / OT-connected hosts; publish MQTTS to cloud mqtt. |
The real image set is openfdd-central, openfdd-web, openfdd-fieldbus, openfdd-mqtt, and openfdd-mcp. There is no openfdd-commission or openfdd-mcp-rag service in the product stack.
Prerequisite: GHCR pull access
Railway must be able to pull the selected GHCR images.
For the easiest open-source deployment, make the Open-FDD GHCR packages public in GitHub package settings. New GHCR packages can default to private, so re-check visibility whenever a new image/package is introduced.
docker logout ghcr.io 2>/dev/null || true
docker pull ghcr.io/bbartling/openfdd-central:nightly
docker pull ghcr.io/bbartling/openfdd-web:nightly
docker pull ghcr.io/bbartling/openfdd-mqtt:nightly
If package visibility must remain private, Railway supports GHCR registry credentials (plan-dependent). Use a GitHub token scoped to read:packages only. Never embed registry credentials in repository files.
See GHCR images for the image/tag contract.
Create the Railway services
- Create a Railway project.
- Add
openfdd-centralfrom GHCR; attach volume at/workspace; set secrets; deploy; wait for/api/health. - For MQTTS hub: add
openfdd-mqtt(private); provision certs/ACL per mqtt image docs; confirm 8883. - Add
openfdd-webfrom GHCR; set upstream; give only web a public domain; deploy. - Keep central (and mqtt) private; use Railway private networking.
Both central and web listen on container port 8080. Local Compose maps web 3000:8080 for developer convenience only.
Railway private DNS: openfdd-central.railway.internal:8080.
Central variables
OPENFDD_JWT_SECRET=<long deployment-unique random secret, ≥32 chars>
OPENFDD_ADMIN_PASSWORD=<strong deployment-unique password>
OPENFDD_AGENT_PASSWORD=<strong password for FDD AI / MCP agents — not the admin password>
OPENFDD_WORKSPACE=/workspace
OPENFDD_PARQUET_ROOT=/workspace/.cache/parquet
OPENFDD_REACT_UI=1
OPENFDD_UI_GENERATION_DEFAULT=react
Store these only in Railway Variables / Secrets. Never commit them, never paste JWTs into chat transcripts or repo files.
| Identity | Login | JWT role | Use |
|---|---|---|---|
admin |
OPENFDD_ADMIN_PASSWORD |
admin | Browser UI, mint agent tokens, edge kits |
agent |
OPENFDD_AGENT_PASSWORD |
operator | Cursor / MCP / REST FDD assistance |
Prefer agent for remote AI assistance. Do not share the admin password with MCP hosts.
Secure agent auth on Railway (Cursor / MCP)
- Set
OPENFDD_JWT_SECRET,OPENFDD_ADMIN_PASSWORD, andOPENFDD_AGENT_PASSWORDon central. - Keep central (and MCP) on Railway private networking — only
openfdd-webgets a public domain. - Mint a short-lived operator JWT (do not log the token):
# From a trusted shell that can reach central (Railway shell / VPN), either:
# A) agent password login
TOKEN="$(curl -fsS -X POST http://openfdd-central.railway.internal:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"agent\",\"password\":\"$OPENFDD_AGENT_PASSWORD\"}" \
| jq -r '.token // .access_token')"
# B) admin mints a short-lived agent token (ttl_secs default 3600, max 86400)
ADMIN_TOKEN="$(curl -fsS -X POST http://openfdd-central.railway.internal:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":\"$OPENFDD_ADMIN_PASSWORD\"}" \
| jq -r '.token // .access_token')"
TOKEN="$(curl -fsS -X POST http://openfdd-central.railway.internal:8080/api/auth/agent-token \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"ttl_secs":3600}' \
| jq -r '.token // .access_token')"
- Point MCP at private central with that JWT only:
OPENFDD_API_BASE=http://openfdd-central.railway.internal:8080
OPENFDD_MCP_TOKEN=<operator JWT from step 3>
# Writes stay off unless deliberate:
# OPENFDD_MCP_ALLOW_WRITES=1
- Rotate: change
OPENFDD_AGENT_PASSWORD/ re-mint JWT; treat stolen JWTs as compromised until expiry.
GET /api/auth/status reports agent_login_configured when the agent password is set.
For MQTTS hub, also configure Central’s MQTT client settings to the private broker (host/port/certs) per stack env conventions used in Compose — never commit secrets.
Web variables
OPENFDD_CENTRAL_UPSTREAM=openfdd-central.railway.internal:8080
OPENFDD_NGINX_RESOLVER=auto
Do not include http:// in OPENFDD_CENTRAL_UPSTREAM. OPENFDD_NGINX_RESOLVER=auto (image default) picks the first nameserver from /etc/resolv.conf; override with an explicit IP if needed.
Health and verification
curl -fsS http://openfdd-central.railway.internal:8080/api/health
curl -fsS https://<web-domain>/api/health
getent hosts openfdd-central.railway.internal
Then open the web domain, log in, and for CSV labs import an openfdd_package_v1 zip. For MQTTS hubs, confirm Operations MQTT monitor / ingest after fieldbus publishes.
Do not use bare /health; the product route is /api/health.
MCP sidecar
OPENFDD_API_BASE=http://openfdd-central.railway.internal:8080
OPENFDD_MCP_TOKEN=<operator JWT from agent login or POST /api/auth/agent-token>
Keep MCP private. Prefer OPENFDD_AGENT_PASSWORD / mint endpoint over putting OPENFDD_ADMIN_PASSWORD into any MCP host. Leave OPENFDD_MCP_ALLOW_WRITES unset unless an operator explicitly enables mutating tools (confirm:true still required).
OT / BACnet warning
Railway is not a substitute for an OT network. BACnet/IP discovery belongs with on-prem fieldbus. Cloud MQTTS is the transport hub, not a BAS broadcast domain.
GHCR release flow for Railway
A merge to master triggers the stack GHCR publisher (openfdd-central, openfdd-web, openfdd-fieldbus, openfdd-mqtt) and the separate MCP publisher.
Before redeploying Railway:
- wait for publish workflows to finish green;
- resolve the immutable
sha-<7>tag; - verify
:nightlydigest if using the floating channel; - redeploy services (image push alone may not restart Railway).
Security posture
Treat Railway as an experimental lab/demo unless your own controls, TLS, persistence, backups, and exposure policy have been reviewed.
Never:
- commit secrets or OT credentials;
- share
OPENFDD_ADMIN_PASSWORDwith MCP hosts (useOPENFDD_AGENT_PASSWORDor/api/auth/agent-token); - embed long-lived JWTs in Cursor config checked into git;
- expose BACnet write paths publicly;
- publish MQTTS
8883to the open internet without a deliberate ACL/TLS design; - assume a public web domain makes the whole stack production-safe.
Report vulnerabilities via GitHub Private Vulnerability Reporting.
Troubleshooting
nginx: [emerg] host not found in upstream "….railway.internal"
Cause: nginx resolved the upstream hostname at startup before Railway private DNS had the peer.
Fix (tip images): lazy DNS — variable upstream + resolver / OPENFDD_NGINX_RESOLVER=auto. Redeploy web on a nightly built after that change.
Operational:
- Ensure central is deployed and
GET /api/healthis 200. - Confirm
OPENFDD_CENTRAL_UPSTREAMmatches the Railway service name exactly. - From Railway shell:
getent hosts openfdd-central.railway.internal. - Increase healthcheck retry window if central is slow; still prefer deploy-central-first.
- Retry web deploy after central is healthy.
GHCR image will not pull
Confirm package visibility or configure read-only GHCR credentials.
Health check fails (web)
Confirm container port 8080. If / never becomes ready, inspect nginx logs for upstream/DNS errors. Prefer verifying /api/health through the web proxy once nginx is up.
Web loads but /api fails
Confirm OPENFDD_CENTRAL_UPSTREAM, same Railway project/environment, and central on 8080.
Imported data disappears after redeploy
Attach persistent storage for /workspace.
BACnet discovery finds nothing
Expected on generic cloud networks — run fieldbus on the OT LAN and use cloud MQTTS.
One-click template target
Draft notes live under railway/. A verified Railway Template should create central → mqtt → web, generate secrets, attach /workspace, set OPENFDD_CENTRAL_UPSTREAM, keep central/mqtt private, and document first login. Do not add a README “Deploy on Railway” button until that template is published and tested.
Railway currently lacks a first-class dependsOn / deploy-after field for private DNS peers — document sequencing in checklists until the platform supports it.