Self-Hosting
Deploy erxes on your own server using Docker Compose and an HTTPS reverse proxy: a single-server Core UI, Core API, gateway, and logs-service deployment, backed by a MongoDB replica set and persistent Redis. Plugins and external integrations are added after the core installation is working.
The repository does not include a ready-made Compose file. The configuration below is a deployment template built from the Dockerfiles, CI workflows, and runtime code in erxes/erxes, not a bundled installer or a high-availability setup. Supply a working MongoDB endpoint and compatible image references before running it. For running source code on your laptop, use Local Setup.
Deployment layout
Browser
|
| HTTPS :443
v
Host Nginx
|-- / --> 127.0.0.1:3000 --> Core UI container :80
|-- /gateway/ --> 127.0.0.1:4000 --> Gateway container :4000
|
| GraphQL / service proxy
v
Core API :3300
Core API, gateway, and logs-service --> MongoDB replica set
Core API, gateway, and logs-service --> Redis :6379 (private Docker network)
The gateway image (Dockerfile) bundles Apollo Router v1.59.2 and Rover 0.22.0 for GraphQL composition. Core API registers its internal address through Redis. Apollo Router's internal port defaults to 127.0.0.1:50000; it does not need a published host port. See Backend & API Gateway for the full flow.
Prepare the server
Use an x86-64 Linux server, such as Ubuntu 24.04 LTS. The gateway, Core API, automations, and migrations images are published for
linux/amd64only; the logs and plugin API images also publishlinux/arm64. The Compose example pinsplatform: linux/amd64so it behaves identically on ARM hosts that can emulate.Install:
- Docker Engine and the Docker Compose plugin.
- Nginx, with Certbot and its Nginx plugin for HTTPS.
curland OpenSSL for checks and secret generation.
This guide uses Ubuntu's
sites-available/sites-enabledlayout andsystemctl. Adjust those commands for another distribution.For capacity: the repository does not establish a tested minimum CPU, RAM, or disk size. Budget for the Node.js services, Redis, container images, uploads, and your database workload, and measure a staging deployment before choosing production capacity.
For networking:
- Point your application hostname, such as
erxes.example.com, at the server. - Allow inbound HTTPS (
443) and HTTP (80, used for certificate issuance and redirects), plus your administration access. - The example publishes the UI and gateway ports on loopback only. Core API, Redis, and the router stay internal.
- Allow the containers to reach your MongoDB servers. With plugins enabled, browsers also need access to the plugin asset host described in step 8.
Create a deployment directory owned by your deployment account; all Compose commands below run from it:
mkdir -p ~/erxes-deploy cd ~/erxes-deployPrepare MongoDB
Provision a MongoDB replica set before starting erxes: a self-managed set on private infrastructure or a managed MongoDB service. Replica sets support the sessions and transactions some plugins use (
startSessionwrites). A single-node set works for evaluation but provides no redundancy.Obtain a connection URI that:
- Selects your application database, for example
erxes. - Includes authentication and the appropriate
authSource, replica-set, and TLS settings for your database. - Resolves to hosts reachable from the Docker containers, including all replica-set members MongoDB advertises.
- Grants access to the application database and the logs database. The logs service writes to a separate database named
<application-database>_logs, such aserxes_logs; see Background Services.
For a remote replica set, use its normal discovery URI rather than copying the
directConnection=trueURI from the local-development guide. URL-encode special characters in credentials. Verify the URI withmongoshfrom the deployment network before proceeding.Configure database persistence and automated backups with your database tooling. The Compose file manages Redis and erxes services; it does not provision MongoDB.
- Selects your application database, for example
Select compatible container images
CI publishes these image repositories:
Component Image repository Platforms Tag patterns in CI Core UI erxes/erxes-next-uiamd64 latest, branch, short SHA, semverCore API erxes/erxes-next-core-apiamd64 latest,YYYYMMDD-<short-sha>Gateway erxes/erxes-next-gatewayamd64 latest, full commit SHALogs service erxes/erxes-next-logsamd64, arm64 latest,YYYYMMDD-<short-sha>Automations erxes/erxes-next-automationsamd64 latest,YYYYMMDD-<short-sha>Migrations erxes/erxes-next-migrationsamd64 latest,YYYYMMDD-<short-sha>Plugin APIs erxes/erxes-next-<plugin>_apiamd64, arm64 latest,YYYYMMDD-<short-sha>Apps erxes/frontline-widgets,erxes/posclient-front,erxes/help-centeramd64 latest, branch, short SHA, semverThe release.yml workflow additionally re-tags every image above except
erxes/erxes-next-migrationsas:<version>when a release tag is pushed, so versioned tags exist for released versions. Prefer an explicit version tag or digest over floatinglatestso you can roll back; see Upgrades. Record the selected references and digests:docker pull erxes/erxes-next-core-api:CHOSEN_TAG docker image inspect erxes/erxes-next-core-api:CHOSEN_TAG \ --format '{{json .RepoDigests}}'Use the resulting
repository@sha256:...reference in.env. Repeat for each service and check registry availability for your selected tags.If you build your own images, follow each Dockerfile and CI workflow for the version you deploy: Core API and the logs/automations Dockerfiles use prebuilt application and
erxes-api-sharedartifacts (runpnpm nx build erxes-api-sharedand the service build first), while the gateway and Core UI Dockerfiles have their own build stages.Configure the deployment
Create
.envin~/erxes-deploy. Replace everyREPLACE_...value before starting:APP_HOST=erxes.example.com CORE_UI_IMAGE=REPLACE_WITH_CORE_UI_IMAGE_DIGEST CORE_API_IMAGE=REPLACE_WITH_CORE_API_IMAGE_DIGEST GATEWAY_IMAGE=REPLACE_WITH_GATEWAY_IMAGE_DIGEST LOGS_IMAGE=REPLACE_WITH_LOGS_IMAGE_DIGEST MONGO_URL=REPLACE_WITH_AUTHENTICATED_MONGODB_URI REDIS_PASSWORD=REPLACE_WITH_RANDOM_HEX JWT_TOKEN_SECRET=REPLACE_WITH_DIFFERENT_RANDOM_HEXGenerate a separate value for each secret with
openssl rand -hex 32.Keep secrets private and consistent
Keep
.envout of source control. All API services must share the sameJWT_TOKEN_SECRET; it signs team-member sessions, portal tokens, and app tokens, and a mismatch silently invalidates logins. The same applies toREDIS_PASSWORDon every service and the Redis container itself.APP_HOSTis a bare hostname; the Compose file addshttps://when constructingDOMAINandGATEWAY_URL.Create
compose.ymlalongside.env:name: erxes x-api-environment: &api-environment NODE_ENV: production VERSION: os DOMAIN: https://${APP_HOST:?Set APP_HOST} GATEWAY_URL: https://${APP_HOST:?Set APP_HOST}/gateway MONGO_URL: ${MONGO_URL:?Set MONGO_URL} REDIS_HOST: redis REDIS_PORT: "6379" REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} JWT_TOKEN_SECRET: ${JWT_TOKEN_SECRET:?Set JWT_TOKEN_SECRET} services: redis: image: redis:7 restart: unless-stopped environment: REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD} command: - sh - -c - exec redis-server --appendonly yes --maxmemory-policy noeviction --requirepass "$$REDIS_PASSWORD" volumes: - redis-data:/data healthcheck: test: ["CMD-SHELL", "REDISCLI_AUTH=$$REDIS_PASSWORD redis-cli ping | grep -q PONG"] interval: 10s timeout: 5s retries: 12 core-api: image: ${CORE_API_IMAGE:?Set CORE_API_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "3300" LOAD_BALANCER_ADDRESS: http://core-api:3300 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3300/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 20 start_period: 30s logs-service: image: ${LOGS_IMAGE:?Set LOGS_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "3301" LOAD_BALANCER_ADDRESS: http://logs-service:3301 depends_on: redis: condition: service_healthy gateway: image: ${GATEWAY_IMAGE:?Set GATEWAY_IMAGE} platform: linux/amd64 restart: unless-stopped environment: <<: *api-environment PORT: "4000" INTROSPECTION: "false" ports: - "127.0.0.1:4000:4000" depends_on: core-api: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 15s timeout: 5s retries: 20 start_period: 60s core-ui: image: ${CORE_UI_IMAGE:?Set CORE_UI_IMAGE} platform: linux/amd64 restart: unless-stopped environment: NODE_ENV: production REACT_APP_API_URL: https://${APP_HOST:?Set APP_HOST}/gateway ports: - "127.0.0.1:3000:80" volumes: redis-data:Notes on this file:
- The backend reads
REDIS_HOST,REDIS_PORT, andREDIS_PASSWORD; there is noREDIS_URL. Redis holds service-discovery keys and BullMQ queues, so the example enables AOF persistence and thenoevictionpolicy. Pin theredis:7digest too when recording your release. LOAD_BALANCER_ADDRESSmust be reachable inside Docker. Without it, a production service registershttp://plugin-<name>-api:<port>, which does not match this template's service names.VERSION: osselects the open-source code paths used by/initial-setupand plugin discovery.- The Core UI container's entrypoint writes every
REACT_APP_*variable into a browser-readablejs/env.jsat startup. Put only public frontend configuration there; it is a runtime injection, not a build-time bake, so changingREACT_APP_API_URLtakes effect on container restart. INTROSPECTIONstaysfalsein production unless you need schema introspection for frontend development; the gateway warns when it is enabled.
- The backend reads
Start the containers
Validate the configuration without printing resolved credentials, pull the images, and start:
docker compose config --quiet docker compose pull docker compose up -d docker compose ps docker compose logs --tail=100 core-api gateway logs-serviceWait for Core API to register and the gateway to compose its GraphQL schema, then check the loopback health endpoint:
curl --fail http://127.0.0.1:4000/healthIt returns
ok. The gateway only listens after the router is up, so this also confirms initial schema composition; it does not prove every database, worker, or plugin is healthy. Complete the checks in step 7 after HTTPS is working.Configure HTTPS with Nginx
On Ubuntu, install Nginx and Certbot if needed:
sudo apt update sudo apt install nginx certbot python3-certbot-nginxCreate
/etc/nginx/sites-available/erxes, replacingerxes.example.comwith yourAPP_HOST:map $http_upgrade $erxes_connection_upgrade { default upgrade; '' close; } server { listen 80; server_name erxes.example.com; client_max_body_size 50m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location = /gateway { return 301 /gateway/; } location /gateway/ { proxy_pass http://127.0.0.1:4000/; proxy_http_version 1.1; proxy_buffering off; proxy_read_timeout 300s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $erxes_connection_upgrade; } }The trailing slash in
proxy_pass http://127.0.0.1:4000/strips the public/gateway/prefix, so/gateway/graphqlreaches/graphqlon the gateway. The WebSocket upgrade headers carry GraphQL subscriptions on the same path.50mis a proxy request limit, not a guarantee of application upload size; the JSON body limit in the APIs is15mb, and file uploads go through the configured storage backend.Enable the site, test it, and request a certificate:
sudo ln -s /etc/nginx/sites-available/erxes /etc/nginx/sites-enabled/erxes sudo nginx -t sudo systemctl reload nginx sudo certbot --nginx -d erxes.example.com --redirect sudo certbot renew --dry-runCertificate issuance requires working DNS and access to the HTTP challenge endpoint. Verify automatic renewal is scheduled.
Verify and create the owner account
Check the public gateway and GraphQL endpoint:
curl --fail https://erxes.example.com/gateway/health curl --fail https://erxes.example.com/gateway/graphql \ -H 'Content-Type: application/json' \ --data '{"query":"query SelfHostingHealth { __typename }"}'Expect
okfrom health and a GraphQL response withdata.__typenameequal toQuery. HTTP 200 with anerrorsarray is not a successful GraphQL check.Open
https://erxes.example.com. On a fresh database, the frontend fetches/initial-setup, seeshasOwner: false, and redirects to/create-owner. Create the initial owner account, sign in, then verify you can create and edit a record and that it appears without a manual refresh. Inspect browser requests for failed API calls or WebSocket connections.Add plugins and integrations
For each plugin, deploy its matching API image (
erxes/erxes-next-<plugin>_api) built from a compatible version, with the shared API environment plusPORTandLOAD_BALANCER_ADDRESS, and giveENABLED_PLUGINSto both Core API and the gateway as a comma-separated list of base names such assales,operation, with no_api/_uisuffixes, spaces, or trailing commas. Deploy every listed API before recreating core services: the gateway waits for all enabled plugins to register and serve their subgraph. Plugin APIs that join after startup trigger a router recompose through thegateway-update-apollo-routerqueue, but a plugin that never registers leaves the gateway waiting at boot.For automation execution, deploy
erxes/erxes-next-automationsexplicitly;ENABLED_SERVICESis a development-startup variable, not a Compose directive. The logs-service container in this template persists event logs, activity logs, and the undo journal; see Background Services.Plugin UI assets are served from a CDN
Core API's
/get-frontend-pluginsroute returnshttps://plugins.erxes.io/<version>/<plugin>_ui/remoteEntry.js; the asset host is hardcoded.<version>is the plugin API's registeredreleaseVersion(RELEASE_VERSIONwhen it starts with3., elselatest), and the corresponding folder must exist on the CDN. Self-hosting the backend does not make plugin assets local or the deployment offline-capable. Serving plugin UIs from your own infrastructure requires building the*_uiprojects yourself and changing that route. Check the asset URLs and release compatibility before enabling plugins in production. See Frontend & Plugin Loading.Configure the storage backend used by your deployment (Settings → file upload:
UPLOAD_SERVICE_TYPEand provider credentials are stored in the database, not env vars) before relying on uploads, and include stored objects in backups. Email delivery, SSO, and other integrations need their own credentials; see Configuration.Widgets, the client portal template, the POS client, and the help center are separate applications with their own images (
erxes/frontline-widgets,erxes/posclient-front,erxes/help-center) and environment; this template does not launch them.
Operations
Logs and troubleshooting
docker compose ps
docker compose logs --tail=200 gateway core-api logs-service
docker compose logs --tail=100 redis core-ui
- Gateway waits for a plugin: the API container must be running, registered as
erxes-service-<name>in Redis, and reachable at itsLOAD_BALANCER_ADDRESS; the enabled list must have no missing services. - Gateway health works but GraphQL fails: check the router process and
supergraph composeerrors in the gateway logs. - MongoDB connection or authorization error: check container DNS/connectivity, URI encoding, TLS, and permissions on both the application and
_logsdatabases. - Redis authentication error:
REDIS_PASSWORDmust be identical on the container and every API/worker. - Sign-in, CORS, or subscription failure: check the public HTTPS URL,
DOMAIN,REACT_APP_API_URL, proxy prefix stripping, and WebSocket upgrade headers. - Plugin navigation fails: check API registration plus the returned
plugins.erxes.ioasset URL and its version. - Containers exit during startup: check image architecture (
platform: linux/amd64), compatible artifacts, env completeness, and memory.
See Logs & Troubleshooting for more symptoms and fixes, including Redis discovery keys.
Backups and upgrades
Back up the application MongoDB database, its <db>_logs database, uploaded files/object storage, and the deployment configuration. Preserve Redis state for queued work as your recovery plan requires. Store backups off the host and test restoration.
Before an upgrade, review the release's migration notes (Database Migrations), take a restorable backup, and test the new image set with a copy of your data. Then update the image references in .env and apply:
docker compose pull
docker compose up -d
docker compose ps
Repeat the health, GraphQL, login, and plugin checks. See Upgrades for rollback. Restarting an older image does not reverse database migrations, and a single-server Compose update causes a short outage.
Stop or remove the containers
docker compose stop # stop, keep containers and volumes
docker compose down # remove containers and network, keep named volumes
The redis-data volume survives down, and the separately managed MongoDB is unaffected.
Data loss
docker compose down -v deletes the Redis volume (queues and service discovery). Run it only as a deliberate operation after confirming your MongoDB backups, never as part of generic host cleanup.
Licensing
Review the repository's LICENSE.md for the version you deploy. It contains AGPLv3 provisions, references Enterprise Edition terms, and prohibits hosting a SaaS version that competes with erxes Inc. Self-hosting does not by itself grant unrestricted plugin or commercial SaaS rights; Enterprise plugins are licensed separately.