Two single-execution bash scripts that take a fresh Ubuntu 24.04 LTS server from zero to a production-ready Frappe v15 / ERPNext v15 install.
| Script | Stage | What it does |
|---|---|---|
setup_frappe_v15_ubuntu_24.sh |
System prerequisites | Installs git, Python 3.12, MariaDB 10.11, Redis, Node 18 (nvm), Yarn, wkhtmltopdf, frappe-bench |
setup_frappe_production.sh |
Production switch | Installs nginx + supervisor + ansible, runs bench setup production ×2, fixes nginx asset perms, verifies services |
Between the two scripts you run the standard bench commands (bench init, new-site, get-app, install-app) manually — they're kept out of the scripts so you keep full control over site names, branches, and app selection.
- Requirements
- The full deployment flow
- Script 1 —
setup_frappe_v15_ubuntu_24.sh - Manual middle step — bench init + first site
- Script 2 —
setup_frappe_production.sh - Post-production hardening
- Troubleshooting
- Versions installed
- Reference
- OS: Ubuntu 24.04 LTS (fresh install recommended)
- Privilege: A non-root user with
sudo(this user will own the bench) - Network: Outbound HTTPS to GitHub, PyPI, npm, Ubuntu apt mirrors
- Resources: Minimum 2 vCPU, 4 GB RAM, 20 GB disk. Recommended for production: 4 vCPU, 8 GB RAM, 40 GB SSD.
⚠️ Never run as the literalrootaccount. Always invoke viasudofrom your bench user. Both scripts read$SUDO_USERto know where things likenvmand~/frappe-benchlive. Running as root puts those in/root/..., whichbenchwill never find.
┌─────────────────────────────────────────────────────────────────────┐
│ Fresh Ubuntu 24.04 LTS server │
└────────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 1. setup_frappe_v15_ubuntu_24.sh │
│ Installs all system prerequisites │
└────────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 2. bench init frappe-bench --frappe-branch version-15 │
│ bench new-site <site> │
│ bench get-app erpnext --branch version-15 │
│ bench --site <site> install-app erpnext │
│ (run as the bench user, NOT as root) │
└────────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 3. setup_frappe_production.sh │
│ Switches the bench into production mode │
└────────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 4. (manual) SSL, firewall, backups, patched-Qt wkhtmltopdf │
└─────────────────────────────────────────────────────────────────────┘
| # | Step | Outcome |
|---|---|---|
| 1 | git |
Source control for bench and apps |
| 2 | python3-dev |
Python headers for compiling C extensions |
| 3 | python3-setuptools, python3-pip |
Python package management |
| 4 | python3.12-venv |
Virtualenv support for the bench's env/ |
| 5 | mariadb-server + secure install |
DB engine, root password set, anonymous users/test DB/remote root removed |
| 6 | libmysqlclient-dev |
MySQL C client headers for Python's mysqlclient |
| 7 | 50-server.cnf |
Writes Frappe-required utf8mb4 + barracuda settings, restarts MariaDB |
| 8 | redis-server |
Caching, queues, real-time events |
| 9 | nvm + node 18 (LTS) |
Installed in the non-root user's $HOME/.nvm |
| 10 | npm (apt) + yarn (global) |
Frontend tooling |
| 11 | xvfb, libfontconfig, wkhtmltopdf |
PDF rendering stack (stock — see hardening) |
| 12 | frappe-bench |
bench CLI installed system-wide via pip3 --break-system-packages |
- Run
bench init,bench new-site,bench get-app,bench install-app— those are the manual middle step. - Install nginx, supervisor, or ansible — those belong to Script 2.
- Replace stock
wkhtmltopdfwith the patched-Qt build.
# Recommended: non-interactive
export MYSQL_ROOT_PASSWORD='YourStrongRootPass'
sudo -E bash setup_frappe_v15_ubuntu_24.sh
# Or interactive (prompts for MariaDB root password)
sudo bash setup_frappe_v15_ubuntu_24.sh-E preserves your env var so the script can read MYSQL_ROOT_PASSWORD.
The script automates mysql_secure_installation with the exact answers from the source guide:
| Prompt | Applied |
|---|---|
| Set/change root password | ✅ Set to $MYSQL_ROOT_PASSWORD, with unix_socket retained as fallback so local sudo mysql still works |
| Remove anonymous users | ✅ |
| Disallow root login remotely | ✅ |
Remove test database and access to it |
✅ |
| Flush privileges | ✅ |
50-server.cnf is overwritten with the Frappe-required stanzas. The original is preserved at /etc/mysql/mariadb.conf.d/50-server.cnf.orig (first run only).
Run these as your non-root user (the one that invoked sudo for Script 1):
# 1. Initialize bench (creates ~/frappe-bench)
bench init frappe-bench --frappe-branch version-15
cd frappe-bench
# 2. Create your first site
bench new-site mysite.local
# → enter the MariaDB root password from Script 1
# → set the Administrator password
# 3. Pull ERPNext
bench get-app erpnext --branch version-15
# 4. Install ERPNext on the site
bench --site mysite.local install-app erpnext
# 5. (Optional) verify in dev mode
bench start
# → http://mysite.local:8000 (after `bench --site mysite.local add-to-hosts`)If
bench initfails with "Node version >= 18 required", you haven't sourced nvm in this shell. Runsource ~/.profile(or log out and back in) and retry.
Once dev mode works, stop bench start (Ctrl+C) before running Script 2.
- Optional — create a new linux user dedicated to running the bench (
adduser,usermod -aG sudo, set password). - Validate that the target user exists and their
~/frappe-benchis a real bench (hasProcfile+sites/). - Install nginx + supervisor (required by
bench setup production). - Install ansible via
sudo /usr/bin/python3 -m pip install ansible --break-system-packages—bench setup productioninvokes ansible playbooks internally and aborts without it. - Run
bench setup production <user> --yestwice — first pass writes the nginx + supervisor configs, second pass confirms an idempotent end state (catches the documented first-run quirk where supervisor doesn't reread the new conf file). - Apply
chmod o+rxon the user's$HOMEand on the bench dir — this is the #1 cause of blank CSS/JS on a fresh production switch, becausenginxruns aswww-dataand can't traverse intosites/assets/without world-execute on every parent. nginx -t→systemctl reload nginx→ status check.supervisorctl reread && update && status— full refresh in case the secondbench setup productionpass added new program groups.
sudo -E bash setup_frappe_production.shsudo BENCH_USER=frappe \
BENCH_PATH=/home/frappe/frappe-bench \
bash setup_frappe_production.shUse this when the bench owner isn't the user invoking sudo (e.g. you provisioned as ubuntu but the bench lives under /home/frappe).
sudo CREATE_BENCH_USER=yes \
BENCH_USER=frappe \
BENCH_PASSWORD='Strong#Pass123' \
bash setup_frappe_production.shThis creates the user, adds them to sudo, sets their password, then exits with instructions. You then:
su - frappe- Re-run Script 1 as that user
- Run
bench init/new-site/ etc. as that user - Re-run Script 2 (without
CREATE_BENCH_USER) to actually switch to production
A documented gotcha in Frappe production setup: the first invocation may write /etc/supervisor/conf.d/frappe-bench.conf after supervisor has already cached its config view. The second pass forces a clean state — Frappe's own docs and community runbooks call this out. With --yes both passes are non-interactive.
When you visit https://yoursite/assets/frappe/js/desk.min.js, nginx (running as www-data) tries to open /home/<user>/frappe-bench/sites/assets/frappe/js/desk.min.js. For that, every directory in the path needs the execute (x) bit for "others". Without o+rx on /home/<user> and /home/<user>/frappe-bench, you get HTTP 403/404 on every static asset — site loads but is unstyled.
Script 2 gets you to a working production bench on HTTP. To complete the stack:
Ubuntu 24.04's apt wkhtmltopdf is the unpatched-Qt build and produces broken PDFs (page breaks, headers, margins). Replace it:
ARCH=$(dpkg --print-architecture) # amd64 or arm64
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_${ARCH}.deb
sudo apt install -y ./wkhtmltox_0.12.6.1-3.jammy_${ARCH}.deb
wkhtmltopdf --version # must say: with patched qtsudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot
cd ~/frappe-bench
sudo bench setup lets-encrypt mysite.example.comThis requires your domain's A record to already point to the server.
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
⚠️ Make sure your SSH session stays alive after enabling UFW. Test by opening a second SSH connection before closing the first.
# One-off
bench --site mysite.example.com backup --with-files
# Recurring — bench already adds a daily job to the user's crontab via
# bench setup backups
# Confirm:
crontab -l -u $USERFor offsite backups, configure S3:
bench --site mysite.example.com set-config backup_path_db "s3://yourbucket/db"
bench --site mysite.example.com set-config backup_path_files "s3://yourbucket/files"
bench setup backupsIf you ever invoke bench from cron or a non-login context and hit node: command not found, append to ~/.bashrc:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use default >/dev/nullMariaDB didn't start cleanly. Investigate:
sudo systemctl status mariadb
sudo journalctl -u mariadb --no-pager | tail -50pip3 install ... --break-system-packages placed it in /usr/local/bin. Run hash -r or open a fresh shell.
Expected — nvm sources only in login shells. Run source ~/.profile or log out and back in.
You're not sourcing nvm. Either log out/in, or:
export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" && nvm use defaultShouldn't happen — Script 2 installs ansible before invoking bench. If you see this, the install silently failed. Re-run manually:
sudo /usr/bin/python3 -m pip install ansible --break-system-packages
which ansible-playbookNginx can't traverse to /assets/*. Re-apply:
sudo chmod o+rx /home/$USER
sudo chmod o+rx /home/$USER/frappe-bench
sudo systemctl reload nginxWorkers crashed on startup. Check:
sudo tail -100 ~/frappe-bench/logs/*.log
sudo supervisorctl tail frappe-bench-frappe-web stderrCommon causes: site_config wrong, MariaDB not reachable, port 9000 conflict (socketio).
Gunicorn isn't running. Verify:
sudo supervisorctl status frappe-bench-frappe-web
sudo supervisorctl restart frappe-bench-frappe-webAfter a clean state change (renamed bench, changed user), purge before re-running:
sudo rm /etc/supervisor/conf.d/frappe-bench.conf
sudo rm /etc/nginx/conf.d/frappe-bench.conf
sudo supervisorctl reread && sudo supervisorctl update
sudo bash setup_frappe_production.shsudo systemctl stop mariadb
sudo mysqld_safe --skip-grant-tables --skip-networking &
mysql -uroot
# > FLUSH PRIVILEGES;
# > ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPassword';
# > EXIT;
sudo killall mysqld
sudo systemctl start mariadb| Component | Version on Ubuntu 24.04 | Source |
|---|---|---|
| Python | 3.12.x | apt |
| MariaDB | 10.11.x | apt |
| Redis | 7.0.x | apt |
| Node.js | 18.x (latest LTS patch) | nvm |
| Yarn | 1.22.x | npm i -g yarn |
| wkhtmltopdf | 0.12.6 (apt — unpatched Qt) | apt |
| frappe-bench | latest from PyPI | pip3 |
| nginx | 1.24.x | apt (Script 2) |
| supervisor | 4.2.x | apt (Script 2) |
| ansible | latest from PyPI | pip3 (Script 2) |
Frappe v15 will warn that MariaDB 10.11 is "more than 10.8 which is not yet tested." In practice 10.11 is widely used in production with v15 — the warning is benign.
- Frappe Framework: https://github.com/frappe/frappe (branch
version-15) - ERPNext: https://github.com/frappe/erpnext (branch
version-15) - Bench docs: https://docs.frappe.io/framework/user/en/bench
- Production setup docs: https://docs.frappe.io/framework/user/en/bench/guides/setup-production
Use, modify, and redistribute freely. No warranty.