Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Operations

Reference

Service and process manager

The systemd unit and the gunicorn configuration of a Linux node, the two server entry points, and the boot sequence of a worker.

This page describes how a Linux node runs the Cluster ERP server: the systemd unit deploy/cluster-erp.service, the gunicorn configuration deploy/gunicorn.conf.py, and the two entry points src/cluster_erp/server/asgi.py and src/cluster_erp/server/main.py. It ends with the boot sequence of one worker. What the application does inside its start-up is explained in server startup and app factory.

Scope and source

  • Defined in: deploy/cluster-erp.service, deploy/gunicorn.conf.py, src/cluster_erp/server/asgi.py (app), src/cluster_erp/server/main.py (main).
  • Count: the unit table lists every directive of the [Unit], [Service], and [Install] sections; the gunicorn table lists every module-level setting of the configuration file.

Paths of a real node are written as the unit's own names (WorkingDirectory, EnvironmentFile, ReadWritePaths) and not as values.

Entry points

Entry point Used by Behaviour Source
cluster_erp.server.asgi:app gunicorn on a Linux node A module-level application built by create_app(); each worker imports it, nothing is preloaded. src/cluster_erp/server/asgi.py:1-19
python -m cluster_erp.server.main a workstation Runs uvicorn with the factory create_app; reload in debug mode; worker processes only outside debug mode and never on Windows. src/cluster_erp/server/main.py:12-58

main.py passes the proxy settings to uvicorn: proxy_headers from trust_proxy_headers, forwarded_allow_ips from trusted_proxy_ips, the access log switch, and the keep-alive seconds (src/cluster_erp/server/main.py:39-58).

Systemd unit

Directive Value in the unit Meaning Source
After, Wants network online, PostgreSQL Starts after the network and the local PostgreSQL service. deploy/cluster-erp.service:7-8
Type simple The started process is the service. deploy/cluster-erp.service:11
User, Group the service user The process runs without root rights. deploy/cluster-erp.service:12-13
WorkingDirectory the checkout The repository root, where alembic.ini lives. deploy/cluster-erp.service:14
EnvironmentFile the environment file Settings source; read only at start. deploy/cluster-erp.service:15
Environment PYTHONUNBUFFERED=1 Log lines reach the journal at once. deploy/cluster-erp.service:16-18
Environment PLAYWRIGHT_BROWSERS_PATH Location of the Chromium build of the PDF renderer inside the checkout. deploy/cluster-erp.service:19-20
ExecStart gunicorn with deploy/gunicorn.conf.py and cluster_erp.server.asgi:app Starts the master process. deploy/cluster-erp.service:25
ExecReload kill -HUP of the main process gunicorn starts new workers on the current code and retires the old ones. deploy/cluster-erp.service:21-26
KillMode mixed The master gets the stop signal and stops its workers. deploy/cluster-erp.service:27-29
Restart, RestartSec always, 3 systemd restarts the service three seconds after it exits. deploy/cluster-erp.service:30-31
LimitNOFILE 65535 Open-file limit for pools and sockets of several workers. deploy/cluster-erp.service:32-34
TimeoutStopSec 30 Time for in-flight requests before the process is killed. deploy/cluster-erp.service:35-36
ReadWritePaths the backup folder and the state folder The only writable paths: backups, mail spool, attachment storage. deploy/cluster-erp.service:37-38
NoNewPrivileges, PrivateTmp, ProtectSystem, ProtectHome true, true, full, true Standard systemd hardening. deploy/cluster-erp.service:39-43
WantedBy multi-user.target Starts at boot once enabled. deploy/cluster-erp.service:45-46

Gunicorn configuration

Every value comes from the same environment file the application reads (deploy/gunicorn.conf.py:11-13, deploy/gunicorn.conf.py:22-23).

Setting Value Meaning Source
bind CLUSTER_ERP_SERVER_HOST:CLUSTER_ERP_SERVER_PORT Address of the master; the default is the loopback address. deploy/gunicorn.conf.py:26
workers CLUSTER_ERP_SERVER_WORKERS, or one per core up to eight when 0 or unset Number of worker processes. deploy/gunicorn.conf.py:27-30
worker_class uvicorn_worker.UvicornWorker Each worker runs uvicorn. deploy/gunicorn.conf.py:31
preload_app False A reload imports the new code in the new workers. deploy/gunicorn.conf.py:32-34
max_requests, max_requests_jitter 5000, 500 A worker restarts after this many requests plus jitter. deploy/gunicorn.conf.py:35-38
graceful_timeout 30 Seconds for in-flight requests on reload or stop; matches TimeoutStopSec. deploy/gunicorn.conf.py:39-41
timeout 120 Worker timeout in seconds. deploy/gunicorn.conf.py:42
keepalive CLUSTER_ERP_SERVER_KEEP_ALIVE_SECONDS, default 5 Idle keep-alive window behind the proxy. deploy/gunicorn.conf.py:43-46
forwarded_allow_ips, proxy_allow_ips CLUSTER_ERP_TRUSTED_PROXY_IPS, default the loopback address Peers whose forwarded headers are trusted. deploy/gunicorn.conf.py:47-49
accesslog None No gunicorn access log; the application writes its own request log. deploy/gunicorn.conf.py:50-52
errorlog, loglevel, capture_output standard error, info, True The journal captures the output. deploy/gunicorn.conf.py:53-56

Boot sequence

The diagram shows what happens from systemctl start until a worker is ready. Each worker runs the whole sequence; the migration lock makes the workers take turns on the schema step.

flowchart TD
  systemd["systemd cluster-erp.service"] --> master["gunicorn master"]
  master --> w1["worker 1 imports asgi:app"]
  master --> w2["worker N imports asgi:app"]
  w1 --> life["lifespan start"]
  w2 --> life
  life --> mt{"multi_tenant"}
  mt -- "yes" --> lock["upgrade_database_to_head under migration_lock"]
  lock --> ctl["TenantRegistry.ensure_schema and bootstrap operator"]
  mt -- "no" --> budget["connection budget check"]
  ctl --> budget
  budget --> load["registry.load_all and seeds"]
  load --> loops["background loops started"]
  loops --> ready["GET /ready answers 200"]

In prose:

  1. systemd starts the gunicorn master with the configuration file (deploy/cluster-erp.service:25).
  2. The master starts the workers; each imports cluster_erp.server.asgi:app, which calls create_app() (src/cluster_erp/server/asgi.py:17-19).
  3. The lifespan context of each worker starts (src/cluster_erp/server/app.py:58-65).
  4. In multi-tenant mode the worker creates the default database if needed and migrates it to the newest revision inside the advisory migration lock; a second worker waits and then finds nothing to do (src/cluster_erp/server/app.py:99-113, src/cluster_erp/core/tenancy.py:693-720). It then prepares the control plane and the first operator (src/cluster_erp/server/app.py:114-118). A single-tenant worker does not migrate.
  5. The worker compares the worst-case connection demand with the server's max_connections and logs a POOL BUDGET warning when it does not fit (src/cluster_erp/server/app.py:119-128, src/cluster_erp/server/app.py:610-631).
  6. The worker loads every enabled module, seeds sequences and permissions, replays the erasure ledger, and starts the background loops (src/cluster_erp/server/app.py:139-194).
  7. The context yields; /ready probes the database and, in multi-tenant mode, the control plane (src/cluster_erp/server/app.py:196, src/cluster_erp/server/app.py:1476-1504).

Reload and restart

  • systemctl reload sends HUP; new workers import the current code because the application is not preloaded (deploy/gunicorn.conf.py:3-10, deploy/gunicorn.conf.py:32-34).
  • install.sh restarts instead of reloading when the environment file is newer than the running service, when the unit changed, or when requirements.lock changed. A reloaded worker inherits the packages the master already imported (deploy/install.sh:140-144, deploy/install.sh:228-246).

Constraints and validation

  • On Windows main.py ignores server_workers above one and logs a warning (src/cluster_erp/server/main.py:27-35).
  • Reload and worker processes exclude each other in main.py (src/cluster_erp/server/main.py:36-38).

Discrepancies

Terms introduced

Term Code identifier Definition Turkish term Source
migration lock migration_lock A PostgreSQL advisory lock that lets one worker at a time run the boot migration on a database. göç kilidi src/cluster_erp/core/tenancy.py:707-720
connection budget _single_tenant_connection_budget The check that workers times pool plus overflow stays within max_connections. bağlantı bütçesi src/cluster_erp/server/app.py:610-631

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13