Standard and Scale
ITSM Engine can be deployed in two ways. Standard runs the whole desk on one server. Scale runs the same application on several servers for desks with more agents working at the same time than one server can handle.
Both use the same container image, the same database schema and the same features. Scale is not an edition: nothing is unlocked by it, and nothing is paid for. The difference is only how many copies of the application run and which supporting services sit around them.
[!NOTE] Standard is what the installation guide sets up, and where to start. Scale is
deploy/compose.scale.yaml, laid over the Standard file.
| Setup | Measured |
|---|---|
| One Standard server | Handles up to about 3,000 agents working at the same time (see system requirements). |
| Scale, two application servers | Carried 3,000 agents with 95% of requests within 62 ms (load testing). |
Standard
Four containers from deploy/compose.yaml:
| Service | What it does |
|---|---|
| Caddy | Terminates HTTPS, gets and renews certificates, and forwards requests to the desk. A portal on its own domain is one more site in its configuration (portals). |
| ITSM Engine | The application. One Node.js process serves the agent app, portals, hosted forms, the REST API and the MCP server, and runs the background jobs (email, webhooks, automations, service-level timers, housekeeping). |
| PostgreSQL 17 | All data, and the job queue (pg-boss, in its own pgboss schema). |
| Storage | Attachments, logos and exports, in an S3-compatible bucket. The bundled MinIO can be replaced by any S3-compatible service. |
How Standard holds together
- The database holds the state. Sessions, rate-limit counters, the job queue and every record are in Postgres; files are in storage. The application container writes nothing to its own disk and runs with a read-only file system.
- Background work runs in the same process, taken from the queue in Postgres. Scheduled jobs run once, whichever process picks them up.
- Some data is held in memory and refreshed. Installation settings and plugin state are reloaded from the database every 30 seconds. Who is viewing or typing on a case is kept in the application's memory.
- Email goes out through the provider you configure (SMTP or Resend). The desk runs no mail server of its own.
[!TIP] Standard needs no Redis, no message broker and no load balancer. Backups are one database dump and one copy of the storage bucket (backups).
Scale
Scale adds an override file, deploy/compose.scale.yaml, on top of the Standard compose file. From deploy/:
docker compose -f compose.yaml -f compose.scale.yaml up -d --build
| Service | What changes |
|---|---|
| Caddy | Spreads requests over the application servers (Caddyfile.scale). It finds them by name every 10 seconds and stops sending to one that fails its health check. |
ITSM Engine (desk) | APP_SERVERS copies of the same image (2 by default). They answer requests and queue background jobs, but run none, and don't apply migrations. None keeps anything another one needs. |
| Worker | One more copy of the same image. It applies migrations when it starts, before the application servers start, and runs every background job and schedule, so request handling and jobs don't compete for the same CPU. Caddy sends it no requests. |
| PgBouncer | Pools database connections in transaction mode, so many application servers share PGBOUNCER_POOL_SIZE Postgres connections (40 by default). |
| Redis | Short-lived state the application servers share: who is viewing a case, and messages between servers. Nothing is saved to disk, and nothing in Redis needs a backup. |
| PostgreSQL, storage | Unchanged: one database and one bucket, as in Standard. |
What to set
The override sets the addresses for you (configuration):
| Variable | Points at |
|---|---|
DATABASE_URL | PgBouncer |
DATABASE_DIRECT_URL | Postgres, straight |
REDIS_URL | Redis |
In .env, set APP_SERVERS and, if you need, PGBOUNCER_POOL_SIZE; everything else is as for Standard.
Redis can be any Redis-compatible server, such as Valkey or a managed one: point REDIS_URL at it and remove the
redis service.
What makes several servers work
- Presence goes through Redis. With one process, the desk keeps who is viewing or writing on a case in memory. With several, each server keeps it in Redis, so an agent on one server sees a colleague on another.
- Database connections are pooled twice. Each application server keeps a pool of up to
DATABASE_POOL_SIZEconnections (20 by default) to PgBouncer, which sharesPGBOUNCER_POOL_SIZEconnections to Postgres among them. Migrations and the job queue use a direct connection (DATABASE_DIRECT_URL), since PgBouncer's transaction mode can't hold the locks they take. - The application decides from its configuration. With
REDIS_URLset it uses Redis; without it, memory, as Standard does. WithJOBS_WORKER=falseit queues jobs but leaves running them to the worker. No code or feature differs between the two setups. - Settings stay in step. Each server reloads settings from the database, so a change made on one server reaches all of them within 30 seconds, as it does in Standard.
- Access changes count at once, everywhere. Each server keeps people's roles and units in memory, but checks one number in the database on every request; the database raises it whenever roles, teams or units change. Taking someone's access away works on every server from their next click, in both setups.
- Sidebar counts are shared. The open, unassigned, due-soon and held counts are counted once for everyone and kept for 10 seconds, instead of for each person each minute. When a case changes, the server it changed on tells the others through Redis, and they all count again.
- Live updates reach every browser. Each app server listens to Postgres for changes to cases (a direct connection, since PgBouncer can't pass that on) and pushes them to the browsers connected to it, so a reply written on one server shows at once to a colleague on another. Who is on a case goes through Redis.
- Jobs run once. The job queue is in Postgres, and only the worker takes jobs from it, so an email or a scheduled task runs once however many application servers there are.
[!IMPORTANT] Keep
PGBOUNCER_POOL_SIZE, plus 10 for the job queue and a few for migrations and backups, under Postgres'smax_connections(100 by default).
Choosing between them
| Standard | Scale | |
|---|---|---|
| Servers | One | Several application servers, plus the database (often managed) |
| Containers | 4 | 4, plus N application servers, a worker, PgBouncer and Redis |
| Agents working at the same time | Up to about 3,000 | Beyond that, by adding application servers |
| If the application server fails | The desk is down until it restarts | The other servers carry on |
| Backups | Database and storage | The same: Redis holds nothing to back up |
| Upgrades | Restart one container | The worker migrates, then the application servers restart together (upgrades) |
| Features | All | All |
Start with Standard. It is simpler to run, back up and upgrade, and one server goes a long way: the load test measured 3,000 agents online with 95% of requests answered within 124 ms.
Consider Scale when:
- more than about 2,500 agents work at the same time, and the number is growing;
- the desk must stay up while one server is restarted or fails;
- the application container regularly uses more than about 0.7 of a CPU core. One Node.js process uses one core for requests, and that is the first limit Standard reaches.
Moving from Standard to Scale
The data stays where it is: the same database and the same bucket.
Back up → Set APP_SERVERS → Start with both compose files → Worker applies migrations → Application servers start
Application servers start → Standard: move back with compose.yaml alone
-
Back up as usual (backups).
-
In
.env, setAPP_SERVERS(2 to start with). -
Start the stack with both compose files:
docker compose -f compose.yaml -f compose.scale.yaml up -d --buildThe worker starts first and applies any migrations, then the application servers start. Nothing in the data changes.
Moving back
Moving back works the same way in reverse: start the Standard stack with compose.yaml alone:
docker compose up -d --remove-orphans
The worker, PgBouncer and Redis stop, and the one desk container runs jobs again.