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:
- systemd starts the gunicorn master with the configuration file (
deploy/cluster-erp.service:25). - The master starts the workers; each imports
cluster_erp.server.asgi:app, which callscreate_app()(src/cluster_erp/server/asgi.py:17-19). - The
lifespancontext of each worker starts (src/cluster_erp/server/app.py:58-65). - 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. - The worker compares the worst-case connection demand with the server's
max_connectionsand logs aPOOL BUDGETwarning when it does not fit (src/cluster_erp/server/app.py:119-128,src/cluster_erp/server/app.py:610-631). - 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). - The context yields;
/readyprobes 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 reloadsendsHUP; 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.shrestarts instead of reloading when the environment file is newer than the running service, when the unit changed, or whenrequirements.lockchanged. 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.pyignoresserver_workersabove 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 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.