Troubleshooting
Diagnose common self-hosted failures.
Start with the service that logged the error: docker compose logs -f gateway, ... core-api, ... logs-service, ... <plugin>-api, or ... redis. Health probes are curl http://<host>:4000/health (gateway) and curl http://<host>:3300/health (Core API); both return ok when healthy.
Gateway and supergraph
/graphqlnever becomes ready (gateway logsWaiting for plugin <name> to join service discoveryorWAITING FOR: <name> graphql endpoint): the gateway waits for every name ingetPlugins()(coreplus both enabled lists) to register and answer_service { sdl }. CompareENABLED_PLUGINS/ENABLED_PLUGINS_ONLY_APIwith the plugin APIs actually deployed; remove entries whose API is not deployed, or deploy them.MAX_PLUGIN_RETRYbounds retries; once exhausted the gateway exits with code 1.Supergraph composition failed,New supergraph could not be parsed,COLLISION: a subgraph's SDL is broken. Check therover supergraph composeoutput on stderr in the gateway logs and each backend'sPOST _service { sdl }at its registered address, then fix or remove the offending subgraph. Composition runs in every gateway replica and the router reloads in place on success.- A plugin registers but its GraphQL calls fail: the address stored in
erxes-service-<name>is not reachable from the gateway (LOAD_BALANCER_ADDRESSwas inferred ashttp://plugin-<name>-api:<port>in production). SetLOAD_BALANCER_ADDRESSexplicitly, orDELthe staleerxes-service-<name>key and restart the plugin. - Supergraph stays stale after a plugin restarted: registration enqueues a
gateway-update-apollo-routerjob delayed 10 seconds (3 attempts, exponential backoff, deduplicated by thegateway:update-apollo-router:pendinglock). If the lock leaked, delete it and re-register a service. EADDRINUSE :::4000orEACCES: the host port is already bound. Free it or remap- "4001:4000"; services do not retry bind failures.
Check Redis state directly: docker compose exec redis redis-cli -a <password> KEYS 'erxes-service-*' and GET erxesservice:config:<name> (see service-discovery.ts for the key format).
Auth and sessions
- Login works locally but fails behind HTTPS: browsers drop the
secureauth-tokencookie (set wheneverNODE_ENVis nottest/development, with a 14-daymaxAge) over plain HTTP. Serve the UI over HTTPS. Auth also acceptsAuthorization: Bearer,erxes-core-token,erxes-app-token, andx-app-api-tokenheaders;SAME_SITE=noneenables cross-site cookies when the origin differs fromDOMAIN. token auth failed/Invalid signature:JWT_TOKEN_SECRETdiffers between the gateway, core-api, and a plugin. Align the env and restart. The code default isSECRET; never deploy with it.- Requests lose identity through
/pl:*or/graphql: the gateway repacks auth into a base64userheader (setUserHeaderin userMiddleware.ts) and warns once it exceeds 32 KB; oversized headers get dropped by proxies. SetDEBUG_GATEWAY_AUTHto loguser-header-set,user-token-expired, anduser-token-errorper request. A missing header means the request arrived unauthenticated; check token expiry anduser_token_*Redis keys. - Sessions expire or all users are logged out: sessions are Redis-backed (
user_token_<userId>_<token>keys, set with a 24-hour TTL at login). An eviction policy, flush, or Redis restart without AOF invalidates every session. Keepnoevictionand persistence.
Plugins missing or broken
- Plugin absent from navigation: query
GET <core-api>/get-frontend-plugins; if the plugin is not listed with aremoteEntry.jsURL, add the base name toENABLED_PLUGINSand restart core-api.VERSION=saasadditionally filters by purchased charge. remoteEntry.js404: the URL isplugins.erxes.io/<version>/<name>_ui/remoteEntry.js, where<version>comes from the plugin'sRELEASE_VERSIONregistration. Theci-ui-*workflow must have published assets for that version; use aRELEASE_VERSIONstarting with3.or leave it unset forlatest.- A plugin route or widget fails to load in the browser: check the DevTools network tab for the remote entry fetch and
loadRemoteerrors in the console. The runtime remote name uses underscores (pos-client→pos_client_ui) while the CDN path keeps the original name; confirm theremoteEntry.jsURL resolves publicly and the plugin'sconfig.tsxname matches. /pl:<name>returnsService not found: the service name after/pl:must match the registered name, anderxes-service-<name>must hold a reachable address; the gateway proxies/pl:<serviceName>to that address and rewrites the path to/. An empty or stale value produces the 404; fixLOAD_BALANCER_ADDRESSor delete the key and restart.
Data services
Transaction numbers are only allowed on a replica set memberorTransaction ... requires a replica set:MONGO_URLtargets a standalonemongod, but some plugins wrap writes instartSessiontransactions. Initialize a replica set (rs.initiate()) and includereplicaSet=in the URI.- Backend cannot connect after fixing the URI: with
directConnection=truethe URI's host must be reachable from the containers; do not mix127.0.0.1between host and container contexts. WRONGPASS/NOAUTHfrom Redis:REDIS_PASSWORDdoes not match the Redis container'srequirepass. Match them; the code only readsREDIS_HOST,REDIS_PORT,REDIS_PASSWORD, with noREDIS_URL.- Activity logs or undo history empty: the logs service may not be running, or the
<db>_logsdatabase is missing. Logs write to a separate database (erxes→erxes_logs); thelogscollection TTL-deletes afterLOG_RETENTION_DAYS(default 365). - Automations never fire: the
automationsservice must be running anderxes-active-pluginsmust list it. Automations are triggered byredisPubSubevents and BullMQ queues (automations-trigger,-action,-aiAgent); they are not a GraphQL subgraph.
Networking and CORS
- Browser CORS errors: the allow-list is exact origins (
DOMAIN,WIDGETS_DOMAIN, comma-separatedALLOWED_DOMAINS) plus comma-separated regexes inALLOWED_ORIGINS. There is no automatic subdomain wildcard; add tenant hosts viaALLOWED_DOMAINSor a regex. - API calls go to the wrong host:
REACT_APP_API_URLon thecore-uicontainer must be the public gateway URL the browser can reach.<subdomain>inside it is replaced by the hostname's first label at runtime. - Wrong tenant resolved: the subdomain is the hostname's first label, read from the
nginx-hostnameheader, then the forwarded host; a flattened hostname (no labels) resolves to the base domain. - Files upload but links break: uploads are configured through Settings → file upload system configs (
UPLOAD_SERVICE_TYPE,AWS_*,CLOUDFLARE_*, …), not env vars. LocalFILE_SYSTEM_PUBLICmode servesread-file?key=URLs.
After any fix, re-run docker compose ps, curl -fsS http://<host>:4000/health, the __typename GraphQL probe, a fresh login, and one write per enabled plugin. If composition still fails, run rover supergraph compose manually against the same supergraph.yaml inputs the gateway logs.