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: node or python.
  • db_type: mariadb, postgres, or sqlite.
  • 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's site_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-bench log_path for this bench's own application metrics. The host-wide system/DB/slow-query log paths are fixed at cli_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.