# Deployment

Deploy erxes on your own server with Docker Compose: requirements, images, and first run


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](https://github.com/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](https://erxes.io/docs/local-setup).

## Deployment layout

```text
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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/gateway/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](https://erxes.io/docs/architecture-backend-gateway) for the full flow.

### 1. 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/amd64` only; the logs and plugin API images also publish `linux/arm64`. The Compose example pins `platform: linux/amd64` so it behaves identically on ARM hosts that can emulate.

Install:

- [Docker Engine](https://docs.docker.com/engine/install/ubuntu/) and the [Docker Compose plugin](https://docs.docker.com/compose/install/).
- Nginx, with Certbot and its Nginx plugin for HTTPS.
- `curl` and OpenSSL for checks and secret generation.

This guide uses Ubuntu's `sites-available`/`sites-enabled` layout and `systemctl`. 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:

```bash
mkdir -p ~/erxes-deploy
cd ~/erxes-deploy
```

### 2. Prepare 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 (`startSession` writes). 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 as `erxes_logs`; see [Background Services](https://erxes.io/docs/architecture-background-services).

For a remote replica set, use its normal discovery URI rather than copying the `directConnection=true` URI from the local-development guide. URL-encode special characters in credentials. Verify the URI with `mongosh` from 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.

### 3. Select compatible container images

CI publishes these image repositories:

| Component | Image repository | Platforms | Tag patterns in CI |
| --- | --- | --- | --- |
| Core UI | `erxes/erxes-next-ui` | amd64 | `latest`, branch, short SHA, semver |
| Core API | `erxes/erxes-next-core-api` | amd64 | `latest`, `YYYYMMDD-<short-sha>` |
| Gateway | `erxes/erxes-next-gateway` | amd64 | `latest`, full commit SHA |
| Logs service | `erxes/erxes-next-logs` | amd64, arm64 | `latest`, `YYYYMMDD-<short-sha>` |
| Automations | `erxes/erxes-next-automations` | amd64 | `latest`, `YYYYMMDD-<short-sha>` |
| Migrations | `erxes/erxes-next-migrations` | amd64 | `latest`, `YYYYMMDD-<short-sha>` |
| Plugin APIs | `erxes/erxes-next-<plugin>_api` | amd64, arm64 | `latest`, `YYYYMMDD-<short-sha>` |
| Apps | `erxes/frontline-widgets`, `erxes/posclient-front`, `erxes/help-center` | amd64 | `latest`, branch, short SHA, semver |

The [release.yml](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/.github/workflows/release.yml) workflow additionally re-tags every image above **except `erxes/erxes-next-migrations`** as `:<version>` when a release tag is pushed, so versioned tags exist for released versions. Prefer an explicit version tag or digest over floating `latest` so you can roll back; see [Upgrades](https://erxes.io/docs/self-hosting-upgrades). Record the selected references and digests:

```bash
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-shared` artifacts (run `pnpm nx build erxes-api-shared` and the service build first), while the gateway and Core UI Dockerfiles have their own build stages.

### 4. Configure the deployment

Create `.env` in `~/erxes-deploy`. Replace every `REPLACE_...` value before starting:

```dotenv
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_HEX
```

Generate a separate value for each secret with `openssl rand -hex 32`.

> [!WARNING]
> **Keep secrets private and consistent**
>
> Keep `.env` out of source control. All API services must share the same `JWT_TOKEN_SECRET`; it signs team-member sessions, portal tokens, and app tokens, and a mismatch silently invalidates logins. The same applies to `REDIS_PASSWORD` on every service and the Redis container itself.

`APP_HOST` is a bare hostname; the Compose file adds `https://` when constructing `DOMAIN` and `GATEWAY_URL`.

Create `compose.yml` alongside `.env`:

```yaml
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`, and `REDIS_PASSWORD`; there is no `REDIS_URL`. Redis holds service-discovery keys and BullMQ queues, so the example enables AOF persistence and the `noeviction` policy. Pin the `redis:7` digest too when recording your release.
- `LOAD_BALANCER_ADDRESS` must be reachable inside Docker. Without it, a production service registers `http://plugin-<name>-api:<port>`, which does not match this template's service names.
- `VERSION: os` selects the open-source code paths used by `/initial-setup` and plugin discovery.
- The Core UI container's [entrypoint](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/frontend/core-ui/docker-entrypoint.sh) writes every `REACT_APP_*` variable into a browser-readable `js/env.js` at startup. Put only public frontend configuration there; it is a runtime injection, not a build-time bake, so changing `REACT_APP_API_URL` takes effect on container restart.
- `INTROSPECTION` stays `false` in production unless you need schema introspection for frontend development; the gateway warns when it is enabled.

### 5. Start the containers

Validate the configuration without printing resolved credentials, pull the images, and start:

```bash
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 core-api gateway logs-service
```

Wait for Core API to register and the gateway to compose its GraphQL schema, then check the loopback health endpoint:

```bash
curl --fail http://127.0.0.1:4000/health
```

It 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.

### 6. Configure HTTPS with Nginx

On Ubuntu, install Nginx and Certbot if needed:

```bash
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
```

Create `/etc/nginx/sites-available/erxes`, replacing `erxes.example.com` with your `APP_HOST`:

```nginx
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/graphql` reaches `/graphql` on the gateway. The WebSocket upgrade headers carry GraphQL subscriptions on the same path. `50m` is a proxy request limit, not a guarantee of application upload size; the JSON body limit in the APIs is `15mb`, and file uploads go through the configured storage backend.

Enable the site, test it, and request a certificate:

```bash
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-run
```

Certificate issuance requires working DNS and access to the HTTP challenge endpoint. Verify automatic renewal is scheduled.

### 7. Verify and create the owner account

Check the public gateway and GraphQL endpoint:

```bash
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 `ok` from health and a GraphQL response with `data.__typename` equal to `Query`. HTTP 200 with an `errors` array is not a successful GraphQL check.

Open `https://erxes.example.com`. On a fresh database, the frontend fetches `/initial-setup`, sees `hasOwner: 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.

### 8. 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 plus `PORT` and `LOAD_BALANCER_ADDRESS`, and give `ENABLED_PLUGINS` to **both Core API and the gateway** as a comma-separated list of base names such as `sales,operation`, with no `_api`/`_ui` suffixes, 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 the `gateway-update-apollo-router` queue, but a plugin that never registers leaves the gateway waiting at boot.

For automation execution, deploy `erxes/erxes-next-automations` explicitly; `ENABLED_SERVICES` is 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](https://erxes.io/docs/architecture-background-services).

> [!WARNING]
> **Plugin UI assets are served from a CDN**
>
> Core API's `/get-frontend-plugins` route returns `https://plugins.erxes.io/<version>/<plugin>_ui/remoteEntry.js`; the asset host is hardcoded. `<version>` is the plugin API's registered `releaseVersion` (`RELEASE_VERSION` when it starts with `3.`, else `latest`), 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 `*_ui` projects yourself and changing that route. Check the asset URLs and release compatibility before enabling plugins in production. See [Frontend & Plugin Loading](https://erxes.io/docs/architecture-frontend).

Configure the storage backend used by your deployment (Settings → file upload: `UPLOAD_SERVICE_TYPE` and 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](https://erxes.io/docs/self-hosting-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

```bash
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 its `LOAD_BALANCER_ADDRESS`; the enabled list must have no missing services.
- **Gateway health works but GraphQL fails:** check the router process and `supergraph compose` errors in the gateway logs.
- **MongoDB connection or authorization error:** check container DNS/connectivity, URI encoding, TLS, and permissions on both the application and `_logs` databases.
- **Redis authentication error:** `REDIS_PASSWORD` must 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.io` asset URL and its version.
- **Containers exit during startup:** check image architecture (`platform: linux/amd64`), compatible artifacts, env completeness, and memory.

See [Logs & Troubleshooting](https://erxes.io/docs/self-hosting-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](https://erxes.io/docs/self-hosting-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:

```bash
docker compose pull
docker compose up -d
docker compose ps
```

Repeat the health, GraphQL, login, and plugin checks. See [Upgrades](https://erxes.io/docs/self-hosting-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

```bash
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.

> [!WARNING]
> **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](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/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.

