Skip to content

High Availability & Redundancy

CxReports can run as two or more instances behind a load balancer, so that if one instance becomes unavailable your reporting service keeps running. This is optional — by default CxReports runs as a single instance, and that setup is unchanged.

When to use this

Enable High Availability (HA) mode if you need redundancy (no downtime when an instance restarts or fails) or want to spread load across several instances. A single instance is fine for most workloads.

What you need

  • A shared PostgreSQL database — every instance connects to the same one.
  • A NATS server — the instances use it to stay in sync (cache updates, live report progress, etc.). For production, run NATS as a small cluster (3 nodes) so it isn't a single point of failure.
  • A load balancer in front of the instances.

No Redis or other extra infrastructure is required.

Turning it on

Set these on every instance (shown as environment variables; in appsettings.json use : instead of __):

Setting Value
HighAvailability__Enabled true
Nats__Enabled true
Nats__Url nats://nats:4222 (your NATS server)
ConnectionStrings__Database the shared database connection string

All other settings (encryption keys, root user, etc.) are configured exactly as for a single-instance Docker deployment and must be identical on every instance.

Important deployment notes

Important

  • Run the same CxReports image/version on every instance. Mixing versions behind one load balancer can break the app in the browser.
  • Instances are interchangeable — any instance can serve any user. This is what lets the load balancer fail over to another instance seamlessly, so do not enable "sticky sessions" on the load balancer.
  • Allow WebSocket connections through the load balancer — they're used for live report-generation progress.
  • Don't point the app's internal URL at the load balancer. Leave the instance URL at its default; CxReports renders PDFs by calling itself on its own local address.
  • Make sure PostgreSQL allows enough connections for all instances combined.

Example: failover with two instances (Docker Compose)

This example runs a primary and a standby instance behind nginx. The standby only receives traffic if the primary becomes unavailable. (For production, use a managed PostgreSQL and a NATS cluster instead of single containers.)

x-app: &app
  image: codaxy/cx-reports:latest
  restart: always
  environment:
    ConnectionStrings__Database: "Host=db;Port=5432;Database=cxreports;Username=postgres;Password=password"
    HighAvailability__Enabled: "true"
    Nats__Enabled: "true"
    Nats__Url: "nats://nats:4222"
    # The same encryption keys and root user must be set on every instance:
    Encryption__Key: "<32-char-key>"
    Encryption__Vector: "<16-char-vector>"
    RootUser__Email: "[email protected]"
    RootUser__Password: "<password>"
    ForwardedHeaders__KnownNetworks__0: "10.0.0.0/8"   # trust your load balancer's network
  depends_on:
    db: { condition: service_healthy }
    nats: { condition: service_started }

services:
  app1:
    <<: *app
  app2:
    <<: *app

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: cxreports
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  nats:
    image: nats:2.11-alpine
    # For production, run a NATS cluster (3 nodes) instead of a single container.

  lb:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    depends_on:
      - app1
      - app2

volumes:
  postgres_data:

nginx.confactive/passive failover: app1 serves all traffic; app2 takes over only if app1 is down.

events {}
http {
  upstream cxreports {
    server app1:8080;            # primary
    server app2:8080 backup;     # standby — used only if the primary is unavailable
  }

  server {
    listen 80;
    location / {
      proxy_pass http://cxreports;
      proxy_http_version 1.1;

      # WebSocket support (live report progress)
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_read_timeout 300s;

      # If the primary errors or times out, retry on the standby
      proxy_next_upstream error timeout http_502 http_503 http_504;
    }
  }
}

Load balancing vs. failover

The example above is failover (one active instance, one standby). If you'd rather spread load across instances, remove the backup keyword — nginx will then distribute requests across both. Either way works with CxReports; choose what fits your needs.

Monitoring your instances

Once HA is running, a system administrator (root user) can see the live state of the deployment under System Administration → HA Instances. The page lists every running instance with:

  • whether it's active (sending heartbeats) or stale,
  • which instance you're currently connected to,
  • the version and uptime of each instance,
  • which instance currently holds each background-work leader role (for example, the email dispatcher).

The view refreshes automatically, so you can watch instances join or drop out — handy for confirming a rollout or a failover.

Licensing in High Availability

In HA mode, each instance registers as its own licensing server. This keeps things simple and resilient — every instance is fully licensed on its own — but it means your license key must allow more than one server. If you're not sure whether yours does, please get in touch with support and we'll help.

Here's what to expect:

  • Provide the license key through configuration, using the LicenseConfiguration__Key setting (or LicenseConfiguration:Key in appsettings.json) — set it identically on every instance, just like the other HA settings above.
  • Each instance activates itself automatically on startup. You do not activate instances one-by-one in the UI.
  • Instances are automatically released when decommissioned. When you scale down or remove an instance for good, its license is freed up after a short grace period, so the slot becomes available again.

Running more instances than your key allows

If you start more instances than your license key permits (beyond a small allowance to ease migrations and rolling deploys), the extra instances fall back to the free tier, and a warning appears under System Administration → HA Instances. Add the needed servers to your key — or reduce the instance count — to clear it.

Tip

A system administrator can also manually deactivate a stale instance's license from the HA Instances page — useful if an instance was removed abruptly and you want to free its slot immediately rather than waiting for the automatic cleanup.

What happens if something fails

  • An instance goes down → the load balancer sends traffic to a healthy instance. Nothing is lost — all data lives in the shared database, and background tasks (scheduled reports, email sending) automatically continue on a remaining instance.
  • NATS is temporarily unavailable → CxReports keeps working; instances simply re-sync once it reconnects.
  • PostgreSQL is the core dependency — run it highly available (a managed, replicated database is recommended).

Limitation

  • Custom uploaded fonts are stored on an instance's local disk and aren't automatically shared. If you upload custom fonts, mount a shared folder for the font directory across all instances.

Hosting on a cloud provider

Tip

If you'd prefer not to operate a multi-instance setup yourself, our managed cloud packages handle high availability, scaling, backups, and monitoring for you.