Configuration
August 7, 2026 ยท View on GitHub
bench.toml is the source of truth for a bench. Read and write it through the config model and TOML store. BenchConfig is also the sole reader/writer of common_config.toml, the host-shared file described in Common Config.
Minimal Example
[bench]
name = "main"
python = "3.11"
http_port = 8000
socketio_port = 9000
socketio_backend = "node"
db_type = "mariadb"
[[apps]]
name = "frappe"
repo = "https://github.com/frappe/frappe"
branch = "version-15"
[redis]
cache_port = 13000
queue_port = 11000
[[workers]]
queues = ["default", "short", "long"]
count = 1
[bench]
name: required bench name.python: required Python version.http_port: web port for local runtime.socketio_port: websocket port.socketio_backend:nodeorpython.db_type:mariadb,postgres, orsqlite.default_branch: optional branch default for new apps.allow_developer_mode: allows developer mode to be toggled per site. Developer mode itself stays in each site'ssite_config.json.watch_apps_js,watch_admin_js,reload_python: development toggles.
Apps
Each [[apps]] entry records one app:
[[apps]]
name = "erpnext"
repo = "https://github.com/frappe/erpnext"
branch = "version-15"
branches = ["version-15", "develop"]
The first app is treated as the framework app when code needs that distinction.
Databases
config.mariadb and config.postgres describe how a bench connects to the chosen engine. existing = true means the user supplied the service and Pilot should not infer or manage it as owned state. Both live in common_config.toml, not bench.toml - see Common Config.
One bench uses one database engine for its sites. Pick it with bench.db_type.
Redis And Workers
[redis] has separate cache and queue ports. They must be distinct.
Workers use [[workers]] array entries:
[[workers]]
queues = ["default", "short", "long"]
count = 2
Production
[production]
enabled = true
process_manager = "systemd"
use_companion_manager = false
Supported process managers are systemd and supervisor.
Admin
[admin]
enabled = true
port = 7000
domain = "admin.example.com"
tls = true
allow_bench_management = true
admin.internal_port is derived as port + 1 for the localhost Gunicorn service behind nginx.
allow_bench_management gates creating and managing sibling benches from this bench's Admin. It defaults to true only on a development checkout (install.sh --dev). A release install defaults to false, so set it in bench.toml to turn it on.
password is stored as a PBKDF2-HMAC-SHA256 hash ($pbkdf2-sha256$<iterations>$<salt>$<key>, hashlib only - no dependency), so bench.toml holds a verifier rather than the password. Set it with pilot set-admin-password or the Settings page; a bench upgraded from an older version is migrated by the hash_admin_password patch, and its cleartext keeps working until then.
jwt_secret is this bench's own local token signing secret, kept in bench.toml. jwks_url and jwks_audience trust a remote issuer instead and are host-shared - see Common Config.
Other Groups
[monitor]: per-benchlog_pathfor this bench's own application metrics. The host-wide system/DB/slow-query log paths are fixed atcli_root()/system/logs/*and not configurable anywhere.[gunicorn]: Gunicorn process settings.[firewall]: firewall behavior.[waf]: WAF behavior.[s3]: S3 backup credentials and bucket settings.[llm]: LLM provider settings used by the admin assistant.
Nginx has no per-bench bench.toml section - config.nginx always holds its compiled-in defaults (ports 80/443, platform-default config_dir, etc.); nothing in bench.toml can override it.
Unknown fields are ignored by normal loads for compatibility. Strict validation can report unknown config paths.
Database Credentials
mariadb.root_password and postgres.root_password never reach a command line. Pilot's own client calls pass them through MYSQL_PWD/PGPASSWORD, and the frappe commands that set a site up (new-site, restore, reinstall, drop-site) get a throwaway MariaDB account instead: MariaDBManager.temporary_setup_user grants it RELOAD, CREATE USER, and full rights on that one site database, then drops it when the command returns. Pilot therefore names the site database itself (_<16 hex>) rather than letting frappe pick a random one. Postgres still passes the superuser credential, because frappe's Postgres setup needs privileges a scoped role cannot hold.
Fetched Endpoints
admin.jwks_url, central.endpoint, datum.endpoint, and llm.api_base are URLs this bench requests itself, so BenchConfig.validate sends each through validate_external_url: the scheme must be http or https, credentials must not be embedded, and the host must not be link-local or a cloud metadata name. Loopback and private addresses stay allowed - a self-hosted model or a local JWKS issuer is a normal setup. Validation reads the literal host only; a domain that resolves to a blocked address is not caught.
Common Config
Some settings are shared by every bench under one benches directory, not owned by any single bench: one MariaDB server, one Postgres server, one ACME account, one trusted admin JWKS issuer, one Central enrolment, one metrics destination. These live in common_config.toml, next to the bench folders, not in any bench's own bench.toml:
[mariadb]
host = "localhost"
port = 3306
admin_user = "root"
root_password = ""
socket_path = ""
existing = false
[postgres]
host = "localhost"
port = 5432
admin_user = "postgres"
root_password = ""
existing = false
[letsencrypt]
email = "ops@example.com"
webroot_path = "/var/www/letsencrypt"
[central]
endpoint = "https://central.example.com"
auth_token = ""
[datum]
endpoint = "https://datum.internal"
token = ""
[admin]
jwks_url = "https://issuer.example.com/jwks.json"
jwks_audience = "bench-fleet"
BenchConfig is the only reader/writer of this file - it merges these values into config.mariadb, config.postgres, config.letsencrypt, config.central, config.datum, and config.admin.jwks_url/jwks_audience on every read, and writes them back on save. Other code reaches these values through a bench's own BenchConfig, never by reading common_config.toml directly. admin.tls is not part of this file - it stays a per-bench choice in bench.toml.
[datum] is where the monitor ships metrics. With both endpoint and token set, and the optional datum package installed (pip install pilot[metrics]), every collection tick is posted as one batch of samples. The JSON-Lines monitor logs are written either way - they stay the Admin UI's source of truth.
The host-wide system/DB/slow-query monitor log paths (system_log_path/db_log_path/slow_query_log_path) are not configurable at all, in bench.toml or common_config.toml - they're fixed at cli_root()/system/logs/{system-stats.log,db-stats.log,slow-queries.json} (see pilot/config/monitor.py).
A pre-upgrade bench whose bench.toml still carries these fields directly is migrated by the merge_common_config patch - see pilot/patches and pilot admin run-patches.