Back to all posts

Frappe & ERPNext Journey ยท Episode 4

Inside frappe-bench: Apps, Sites, Configs and Multi-Tenancy Explained

#Frappe#ERPNext#Frappe Framework#Bench#Multi-Tenancy#frappe bench folder structure#frappe sites folder#site_config.json#common_site_config.json#frappe multi tenancy#bench new-site#bench set-config#frappe dns multitenant#erpnext multi company vs multi site#frappe backup restore#encryption_key frappe#erpnext developer#frappe for beginners
Inside frappe-bench: Apps, Sites, Configs and Multi-Tenancy Explained
November 3, 20268 min read

Every spinning mill has the same layout, even if the buildings look different: a raw material store, the production floor, a finished goods warehouse, and an office with the record books. Once you know the layout, you can walk into any mill and find what you need.

frappe-bench/ is the same. Every Frappe server in the world has the same folder layout. Learn it once, and you can SSH into any ERPNext server and know exactly where the code, the data, the configs and the logs are.

The frappe-bench folder: apps for code, sites for data, plus config, logs, env and the Procfile

Keywords

  • Bench: Both the CLI tool and the folder that holds everything (frappe-bench/).
  • App: A Python package with DocTypes and code: frappe, erpnext, hrms, or your own custom app.
  • Site: One running ERPNext instance with its own database, files and config.
  • Multi-tenancy: One set of code serving many sites (tenants), each with separate data.
  • site_config.json: One site's settings (database name, password, flags).
  • common_site_config.json: Settings shared by every site on the bench.
  • encryption_key: The key Frappe uses to encrypt stored passwords. Lose it, and those passwords can't be decrypted.

Character Mapping

  • apps/: The machinery. Same machines for every customer.
  • sites/: The customers' private warehouses, one per customer.
  • site_config.json: The label on each warehouse door: which room, which key.
  • common_site_config.json: The mill-wide rules posted at the main gate.
  • config/, logs/, env/: The power room, the logbooks, and the tool shed.

1. The Tour

text
frappe-bench/
โ”œโ”€โ”€ apps/                     # code (each one is a git repository)
โ”‚   โ”œโ”€โ”€ frappe/
โ”‚   โ”œโ”€โ”€ erpnext/
โ”‚   โ”œโ”€โ”€ hrms/
โ”‚   โ””โ”€โ”€ mill_erp/             # our custom app
โ”œโ”€โ”€ sites/                    # data and per-site settings
โ”‚   โ”œโ”€โ”€ apps.txt              # apps available on this bench
โ”‚   โ”œโ”€โ”€ common_site_config.json
โ”‚   โ”œโ”€โ”€ assets/               # built JS/CSS (symlinks to each app's public/)
โ”‚   โ”œโ”€โ”€ mill.localhost/
โ”‚   โ”‚   โ”œโ”€โ”€ site_config.json
โ”‚   โ”‚   โ”œโ”€โ”€ public/files/     # uploaded files anyone can open
โ”‚   โ”‚   โ”œโ”€โ”€ private/files/    # uploaded files that need login
โ”‚   โ”‚   โ””โ”€โ”€ private/backups/  # bench backup writes here
โ”‚   โ””โ”€โ”€ test.localhost/
โ”œโ”€โ”€ config/                   # redis_cache.conf, redis_queue.conf (+ supervisor.conf, nginx.conf in production)
โ”œโ”€โ”€ logs/                     # worker.log, worker.error.log, scheduler.log, frappe.log ...
โ”œโ”€โ”€ env/                      # Python virtualenv with every app's dependencies
โ””โ”€โ”€ Procfile                  # the 7 processes from Episode 1

apps/: The Code

Every folder is a git repository. bench get-app erpnext clones ERPNext here and installs its Python dependencies into env/. Your custom app lives here too.

โš ๏ธ Never edit files inside apps/frappe or apps/erpnext. The next bench update will overwrite your changes, or the update will fail with conflicts. Put every customization in your own app. That's what Episodes 10, 12 and 14 of this series are about.

sites/: The Data

apps.txt lists the apps available on this bench. Which apps are installed on each site is stored inside that site's own database, so one site can have ERPNext + HRMS while another has only Frappe + a custom app. Check with:

bash
bench --site mill.localhost list-apps

config/, logs/, env/

  • config/: The Redis configs from Episode 1. In production, bench setup production also writes supervisor.conf and nginx.conf here.
  • logs/: The first place to look when a background job fails (worker.error.log).
  • env/: A Python virtualenv. Bench always runs Python from here, never from your system Python.

2. One Bench, Many Sites

Frappe reads the Host header to choose the site folder and database

Here's the clever part. All sites on a bench share the same code in apps/ and the same running processes, but each site has its own database. So how does one web process know which site you want?

Step 1: Your browser requests http://mill.localhost:8000. The HTTP request carries a header: Host: mill.localhost.

Step 2: Frappe looks for a folder named sites/mill.localhost/.

Step 3: It reads that folder's site_config.json to get the database name and password.

Step 4: It connects to that database, and only that database, for the rest of the request.

*.localhost addresses point to your own machine in modern browsers. If one doesn't, run bench --site mill.localhost add-to-hosts. In production, you'd turn on DNS-based multi-tenancy so mill.example.com and client2.example.com route to their own sites through NGINX:

bash
bench config dns_multitenant on
bench setup nginx
sudo service nginx reload

That's the same idea as System Design Episode 1: DNS and Episode 5: Load Balancers: a name decides where the request goes. Giving each tenant its own database is like sharding by customer (Episode 11): a mistake on the test site can't touch live data.


3. Two Config Files, One Rule

site_config.json overrides common_site_config.json

Frappe merges two JSON files for every site:

  1. sites/common_site_config.json: shared settings like Redis URLs, socketio_port, webserver_port and background_workers.
  2. sites/<site>/site_config.json: that site's database credentials, encryption_key, and any site-specific flags.

The rule: if the same key is in both, the site's value wins. (Environment variables beat both.) That's how you can run one site in developer mode while the others don't. Use bench to edit them instead of hand-editing JSON:

bash
bench --site mill.localhost set-config developer_mode 1   # this site only
bench config set-common-config -c background_workers 2  # every site (common_site_config.json)
bench --site mill.localhost show-config                   # see the merged result

In code, you read the merged config with frappe.conf:

python
if frappe.conf.developer_mode:
    frappe.log("Running in developer mode")

๐Ÿ” site_config.json holds the database password and encryption_key. Never commit it to git. And back up the encryption_key with your database backups. If you restore a backup without it, every stored password (email accounts, API secrets, payment gateways) becomes unreadable.


4. Multi-Company or Multi-Site?

Multi-company inside one site vs separate sites on one bench

When the mill's owners opened a dyeing unit, the question came up straight away: new site, or new company?

๐Ÿข Many Companies, One Site๐ŸŒ Many Sites, One Bench
DatabaseOne shared databaseOne database per site
MastersShared items, customers, suppliersCompletely separate
UsersSame login, restricted with User PermissionsSeparate users per site
Inter-companyBuilt in (inter-company invoices, consolidated reports)Needs integration
BackupsOne backup covers everythingBack up and restore each separately
Choose whenRelated companies in one groupSeparate clients, or staging vs live

We chose multi-company: same owners, shared cotton suppliers and yarn buyers, and management wanted one consolidated balance sheet. We still used a separate site for testing (test.localhost), restored from a live backup, so every new feature was tried on real data first.

Everyday Site Commands

bash
bench new-site test.localhost                       # create a site + database
bench --site test.localhost install-app erpnext     # install an app on it
bench use mill.localhost                            # make it the default site
bench --site mill.localhost backup --with-files     # writes to sites/mill.localhost/private/backups
bench --site test.localhost restore <backup.sql.gz> # load that backup into test
bench --site all migrate                            # run patches on every site
bench --site mill.localhost console                 # Python shell inside the site
bench --site mill.localhost mariadb                 # SQL shell on the site's database

๐Ÿ• Explain It Like You're 5

Imagine one big toy box company that builds the same toy box for every family on your street.

Every family gets the same kind of box (the apps), but each family keeps their own toys inside (their database). The name on the box (the site name) tells you whose toys are inside.

That's a bench: one set of boxes, lots of families, and nobody mixes up the toys!


๐ŸŽฏ Debugging Cheat Sheet

SymptomLikely causeFix
"mill.localhost does not exist"Folder name doesn't match the HostCheck sites/ folder names; use add-to-hosts
Wrong site opens on localhost:8000Default site is a different onebench use <site>
My edit in apps/erpnext disappearedbench update reset the repoMove the change into a custom app
Developer mode not workingSet in the wrong config filebench --site <site> show-config
Passwords broken after restoreencryption_key didn't come with the backupCopy the original key into site_config.json
App missing on one site onlyInstalled on the bench, not on that sitebench --site <site> install-app <app>
Job failures with no error shownErrors go to the logslogs/worker.error.log, desk Error Log

Key Takeaways for Interviews

  1. Code in apps/, data in sites/: one codebase, many sites, each with its own database.
  2. The Host header picks the site: folder name โ†’ site_config.json โ†’ database.
  3. Config layering: site_config.json overrides common_site_config.json; read it with frappe.conf.
  4. Protect site_config.json: it holds the DB password and encryption_key, so keep it out of git and back up the key.
  5. Multi-company vs multi-site is a real design decision: shared data and consolidation vs full isolation.

๐Ÿ“š Official Sources

Everything here is checked against Frappe's own documentation and blog:

Next episode, we start the ERPNext module deep dives with Selling: Quotation to Sales Order to Delivery Note to Sales Invoice, and how we added Export LC tracking without breaking upgrades.

Enjoyed this article? Share it:

Series

Frappe & ERPNext Journey

This article is part of the Frappe & ERPNext Journey series. Follow along sequentially for the best learning experience.