</>CodeWithKarani

Anatomy of a Frappe bench

Karani GeoffreyKarani Geoffrey8 min read

The first time I had to fix somebody else's ERPNext server, I ran ls in the bench directory and found five folders, none of which told me what they were for. I guessed. I guessed that apps/ was part of the same repository as everything else, ran git pull at the bench root, got "not a git repository", and concluded the install was broken. It was not broken. I was wrong about its shape.

A bench is not an application. It is a workspace that holds several independent applications, one Python environment, any number of sites, and the generated configuration to run them all. Get the shape right and deployment becomes obvious, because you will know exactly which four paths a deploy touches and which eleven it must not. Get it wrong and you will do what I did on a client's box in 2019: run one command with sudo, create a scatter of root-owned files, and spend a Saturday finding them.

This matters for deploying a Frappe app to a client server you cannot touch, because when you cannot log in and look around, your deploy script has to be right about the layout the first time.

a bench is five directories. The important one is apps/, where every subdirectory is its own independent git clone with its own remote, which is why a deploy is a git fetch inside apps/your_app and not a checkout of the bench.

  • apps/ holds the code, one git repository per app. sites/ holds the data and the per-site configuration.
  • site_config.json carries the database password in plain text and never belongs in version control.
  • env/ and config/ are generated. Rebuild them with bench, never hand-edit the output.
  • Everything is owned by frappe:frappe. One sudo bench and you will be hunting root-owned files for hours.

The five directories

cd /home/frappe/frappe-bench
ls -1
apps
config
env
logs
sites
The structure of a Frappe bench directory A directory tree for a bench at /home/frappe/frappe-bench. Under apps are frappe, erpnext and your_app, each marked as an independent git repository. Under sites are apps.txt, common_site_config.json and the site directory dev.client.local, which contains site_config.json, public files and private backups. The remaining directories are env, config and logs. Annotations record that the whole tree is owned by the frappe user, that each app has its own remote so a deploy fetches only inside apps slash your_app, that site_config.json holds the database password in plain text and must never be committed, and that config is generated by bench rather than hand-edited. /home/frappe/frappe-bench/ ├── apps/ │ ├── frappe/ │ ├── erpnext/ │ └── your_app/ ├── sites/ │ ├── apps.txt │ ├── common_site_config.json │ └── dev.client.local/ │ ├── site_config.json │ ├── public/files/ │ └── private/backups/ ├── env/ ├── config/ └── logs/ git repo git repo git repo Owned by frappe:frappe, top to bottom No exceptions. Not one file. Three separate repositories Each with its own remote and branch. A deploy runs git fetch inside apps/your_app and nowhere else. Bench-wide settings Database host, redis sockets, the socketio port. Shared by every site on this bench. The database password, in plain text This file never goes into git. Not once, not encrypted. Generated, not authored supervisor.conf, nginx.conf and the redis configs are written by bench. Regenerate them, do not edit them.
The bench root is a workspace, not a repository. The version control lives one level down, three times over.

apps/ is not one repository, it is several

This is the insight that makes Frappe deployment simple, and almost every article about it skips straight past.

cd /home/frappe/frappe-bench/apps/your_app
git remote -v
git log --oneline -1
git describe --tags
origin  https://github.com/your-org/your_app.git (fetch)
origin  https://github.com/your-org/your_app.git (push)
a4f19c2 Add supplier ageing report
v1.4.0

That is a normal git clone, in a normal working tree, with a normal remote. So a deployment does not need to understand Frappe at all at this stage. It needs to fetch a tag and check it out:

cd /home/frappe/frappe-bench/apps/your_app
git fetch --tags --prune origin
git checkout --force v1.4.0

Nothing else in the bench moves. apps/frappe stays exactly where it is, on whatever version-15 commit it was on. apps/erpnext likewise. That containment is the reason a routine app deploy is low risk and a framework upgrade is a planned event with a maintenance window: they touch different repositories, and only one of them is yours.

It also means the bench root is not the thing you put under version control, and that your own customisations belong in a fourth directory alongside these rather than inside apps/erpnext. Managing Frappe and ERPNext custom code via GitHub covers creating that app and getting it onto a server.

Which apps a given site actually has installed is a separate question from which apps are present on disk, and the two do drift apart:

cat sites/apps.txt
bench --site dev.client.local list-apps

The first is what this bench knows about. The second is what that site has installed and will run patches for. If your app is on disk and in apps.txt but not in list-apps, then bench migrate will cheerfully do nothing for it and you will spend twenty minutes wondering why your new DocType has not appeared.

sites/, and the file that must never reach git

Everything that is not code lives here. Uploaded documents, generated PDFs, backups, and the per-site configuration.

cat sites/dev.client.local/site_config.json
{
 "db_name": "_a1b2c3d4e5f6a7b8",
 "db_password": "pLa1nT3xtP4ssw0rd",
 "db_type": "mariadb",
 "encryption_key": "kR2n...=",
 "maintenance_mode": 0
}

That is not a mistake in the framework's design, it is how Frappe expects to find its credentials. But it has two consequences you have to build around. The file is readable by anyone who can become the frappe user, which is the point made in the argument that access is a liability rather than a convenience. And it must never enter version control, which in practice means your deploy script has to be careful with git commands that discard local state.

This is why the checkout above uses --force on the app repository only, and why you never run git clean -xdf anywhere near a bench. Inside apps/your_app that would delete compiled assets, which is recoverable. Pointed at the wrong directory it deletes uploaded customer files, which is not.

Also note maintenance_mode sitting in that file. That is the flag bench --site dev.client.local set-maintenance-mode on flips, and knowing it is a plain integer in a plain JSON file is what lets a deploy script recover from a failure that leaves the site stuck behind the maintenance page.

env/, config/ and logs/

env/ is a Python virtual environment, and every bench command that touches Python is really running env/bin/python. It is generated, so it is disposable. When your app adds a dependency to its pyproject.toml, the deploy has to install it:

cd /home/frappe/frappe-bench
bench setup requirements

Skip that step and you get the deployment failure that wastes the most time in this whole series: bench migrate fails with a ModuleNotFoundError for a package that is right there in your pyproject.toml, because the requirement was declared but never installed into the environment the site actually runs in.

config/ holds supervisor.conf, nginx.conf, the redis configs and a pids/ directory. All generated by bench setup commands from templates. If you hand-edit config/supervisor.conf, the next person to run bench setup supervisor overwrites your change without warning, usually six months later, usually at the worst time.

logs/ is where you will actually diagnose things when you have no shell on the box. bench.log, worker.log, schedule.log, plus web.error.log and the supervisor output streams. Worth naming explicitly in your handover document, because "send me the last hundred lines of logs/worker.error.log" is a request a client's IT person can act on, and "check the logs" is not.

Ownership, and the Saturday

Every file in that tree is owned by the bench user. On a standard install that is frappe.

The web processes run as frappe. The workers run as frappe. Your deploy script must run as frappe, which is why the runner is installed as a service under that account. The moment something runs as root inside this tree, it creates files root owns, and the next ordinary bench command that tries to write over them fails.

The failure never says "you used sudo". It says permission denied in sites/assets, or a build error, or a worker that will not start. Finding the damage:

find /home/frappe/frappe-bench ! -user frappe -printf '%u %p\n' | head -50

And repairing it:

sudo chown -R frappe:frappe /home/frappe/frappe-bench

The temptation at this point is chmod -R 777, which makes the error go away and creates a worse one. What to do instead when you get permission denied is the short version: fix the owner, not the mode.

That command is safe and it is also an admission. If you are running it, something in your process ran as the wrong user, and fixing the ownership without fixing the process means you will run it again next month.

Supervisor, systemd, and restarting without root

Bench generates a supervisor configuration by default. With the bench at /home/frappe/frappe-bench, the generated program groups are named after the bench directory:

sudo supervisorctl status
frappe-bench-redis:frappe-bench-redis-cache      RUNNING   pid 812, uptime 6 days
frappe-bench-redis:frappe-bench-redis-queue      RUNNING   pid 813, uptime 6 days
frappe-bench-web:frappe-bench-frappe-web         RUNNING   pid 940, uptime 2:14:07
frappe-bench-web:frappe-bench-node-socketio      RUNNING   pid 941, uptime 2:14:07
frappe-bench-workers:frappe-bench-frappe-schedule       RUNNING   pid 955, uptime 2:14:07
frappe-bench-workers:frappe-bench-frappe-short-worker   RUNNING   pid 956, uptime 2:14:07

The trailing colon in a group name restarts every program in that group, which is what a deploy wants:

sudo supervisorctl restart frappe-bench-web: frappe-bench-workers:

Note the sudo. That is the one place a Frappe deploy legitimately needs elevated rights, and it is exactly one command. Granting the frappe user unrestricted sudo to solve it is the wrong answer, and the narrow sudoers rule that solves it properly is in the article on hardening a self-hosted runner.

Bench can also generate systemd units instead, which some hosts prefer. Take the default unless you have a reason. What matters for deployment is not which supervisor you use but that your script has a non-interactive, non-root-shell way to restart the processes.

What a deploy touches, and what it must not

Paths a deploy writes to and paths it must never write to Two columns. On the left, the paths a deploy legitimately changes: the app working tree at apps slash your_app, the Python environment when dependencies change, the built assets under sites slash assets, and the database schema through bench migrate. On the right, the paths it must never write to: site_config.json, uploaded public and private files, the backups directory, the generated configuration under config, and the frappe and erpnext app repositories, which move only during a planned framework upgrade. A deploy writes here A deploy must never write here apps/your_app/ git fetch and checkout of the release tag sites/dev.client.local/site_config.json Server-owned credentials. Read it, never rewrite it. env/ bench setup requirements, when dependencies change sites/dev.client.local/public/, private/ Uploaded customer files. Nothing here is reproducible. sites/assets/ bench build, regenerated from source every time sites/dev.client.local/private/backups/ Written only by bench backup. Never cleaned by a deploy. the site database bench migrate, and this is the irreversible one config/ Generated. Edit the template, regenerate, never patch. apps/frappe/, apps/erpnext/ Only during a planned upgrade, never in a routine deploy Four paths on the left. If your deploy script writes to a fifth, ask why. Everything in the right column is either irreplaceable customer data or a file the server owns and the repository does not.
Left column: reproducible from the repository. Right column: not reproducible from anything, or not yours to change.

Which leaves the one item in the left column that is not like the others. Fetching a tag is reversible - check out the old tag and you are back. Rebuilding assets is reversible. Reinstalling Python dependencies is reversible. bench migrate is not, and pretending otherwise is how people lose an afternoon and occasionally a database. That is what bench migrate actually does, and the rollback you will wish you had.

Frequently asked questions

Is the bench itself a git repository?
No, and this trips up almost everyone. The bench directory is a workspace. Each folder under apps/ is its own independent git clone with its own remote and branch. Running git status at the bench root gets you nothing useful, because there is no repository there to report on.
Why did running a bench command with sudo break everything?
Because it created files owned by root inside a tree owned by frappe. The next command that runs as frappe cannot write over them, and the failure surfaces somewhere unrelated, often as a build error or a permission denied in sites/assets. Fix it with a recursive chown back to frappe:frappe and stop using sudo with bench.
Should site_config.json be in version control?
Never. It holds the site's database name and password in plain text, plus any API keys the site uses. It belongs to the server, not to the repository. If you need per-environment settings in git, keep a template with placeholder values and have the deploy leave the real file alone.
Supervisor or systemd for a Frappe bench?
Supervisor is what bench generates by default and what nearly every production install uses, with a single systemd unit starting supervisor itself. Bench can generate systemd units instead, but unless you have a specific reason, take the default. The one that matters is that whichever you pick, your deploy script needs a way to restart the processes without root.
#Frappe#ERPNext#Bench#Linux#Supervisor
Keep reading

Related articles