While Docker offers a quick containerized setup for ERPNext, many production environments and developers prefer the "Bench" method. This "bare metal" approach gives you granular control over the services, easier debugging, and direct access to the configuration files.
This guide covers installing ERPNext Version 16 on Ubuntu 24.04 LTS using modern tools like uv for Python management and nvm for Node.js.
Before we begin, we need to install the core engines and build tools. We will also create a dedicated frappe user, as running ERPNext directly as root is a significant security risk.
Start by ensuring your package lists and installed packages are up to date.
sudo apt update && sudo apt upgrade -y
You can also increase your swap memory if your server has limited RAM:
sudo fallocate -l 8G /swapfile && \ sudo chmod 600 /swapfile && \ sudo mkswap /swapfile && \ sudo swapon /swapfile && \ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
We need Git, Redis, MariaDB, Nginx, Supervisor, and various development headers.
sudo apt install -y git redis-server mariadb-server mariadb-client \ pkg-config libmariadb-dev gcc build-essential libssl-dev cron \ nginx supervisor python3-dev python3-setuptools python3-pip xvfb libfontconfig nano
ERPNext relies on wkhtmltopdf (with patched Qt) to generate PDFs. Since Ubuntu 24.04 has dropped support for this package and its dependencies (libssl1.1), we must manually install the library and the package.
# 1. Install libssl1.1 (Required dependency not in Noble repos) wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb && \ sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb && \ # 2. Install wkhtmltopdf (Jammy Build) wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb && \ sudo dpkg -i wkhtmltox_0.12.6.1-3.jammy_amd64.deb && \ # 3. Fix any missing font/X11 dependencies sudo apt --fix-broken install -y
ERPNext requires a specific character set configuration (utf8mb4) to function correctly. We will create a dedicated configuration file to ensure these settings are applied without modifying the default system files.
sudo tee /etc/mysql/mariadb.conf.d/99-frappe.cnf > /dev/null <<'EOF' [mysqld] character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [mysql] default-character-set = utf8mb4 EOF
Restart MariaDB to apply changes:
sudo systemctl restart mariadb
Run the security script to set a root password and remove insecure defaults.
sudo mariadb-secure-installation
Follow these responses:
We will create the frappe user, set a password, and prepare the directory. We will also add the www-data user (which Nginx runs as) to the frappe group, ensuring it can read the static assets securely without opening permissions to the whole world.
We also need to ensure the frappe user's home path is correctly added to the $PATH variable for executing local binaries.
# 1. Create the user and set a password sudo useradd -m -s /bin/bash frappe sudo usermod -aG sudo frappe sudo passwd frappe # 2. Add www-data to the frappe group (For Nginx Access) sudo usermod -aG frappe www-data # 3. Create the directory and set ownership sudo mkdir -p /opt/frappe sudo chown -R frappe:frappe /opt/frappe # 4. Secure directory permissions # 750: User(rwx), Group(rx), Others(none). # This allows Nginx (in group) to read, but blocks others. sudo chmod 750 /opt/frappe # 5. Switch to the frappe user sudo su - frappe # 6. Add ~/.local/bin to PATH echo 'export PATH=$PATH:~/.local/bin/' >> ~/.bashrc source ~/.bashrc
Note: All subsequent steps should be run as the
frappeuser unless specified otherwise.
For a production environment, we recommend installing Node.js system-wide using the official NodeSource repository rather than nvm. This prevents path issues with Supervisor/Systemd.
# Download and setup the NodeSource repository curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && \ # Install Node.js sudo apt-get install -y nodejs && \ # Install Yarn sudo npm install -g yarn
We use pure Python 3.14 for this Version 16 setup using uv.
# Install uv curl -LsSf https://astral.sh/uv/install.sh | sh && \ source ~/.bashrc && \ uv python install 3.14 --default
Install the bench command line tool using uv into a managed environment.
uv tool install frappe-bench --python python3.14
Tip: If you encounter "EXTERNALLY-MANAGED" errors with
piplater, creating apipConfigor deleting the EXTERNALLY-MANAGED file (risky) is common, but usinguvorpipx(as we did above) is the cleaner solution.
Before initializing the bench, ensure all core components are correctly installed and accessible by the frappe user.
# Check Python version (Should be 3.14.x) python3.14 --version && \ # Check Node.js version (Should be v24.x) node --version && \ # Check NPM version npm --version && \ # Check Yarn version yarn --version && \ # Check MariaDB version mariadb --version && \ # Check Redis redis-server --version && \ # Check wkhtmltopdf (Should be 0.12.6 with patched qt) wkhtmltopdf --version
If any of these commands fail or return unexpected versions, review the previous steps before proceeding.
Now we initialize the Frappe Bench and install the ERPNext application.
cd /opt/frappe # 1. Initialize Bench bench init frappe-bench --frappe-branch version-16 --python python3.14 cd frappe-bench # 2. Download the ERPNext App bench get-app --branch version-16 erpnext # 3. Create a New Site # Replace 'erp.yourdomain.com' with your actual domain bench new-site erp.yourdomain.com --admin-password 'YourAdminPass' --mariadb-root-password 'YourDBRootPass' # 4. Install ERPNext on the site bench --site erp.yourdomain.com install-app erpnext # 5. Enable the Scheduler bench --site erp.yourdomain.com enable-scheduler
For a production environment, we use Nginx as a reverse proxy and Supervisor to manage the processes.
Enable DNS multitenancy so Bench uses domain names to serve sites.
bench config dns_multitenant on bench setup nginx bench setup supervisor
We need to link the generated configs to the system directories. Run these as a user with sudo access (or use sudo).
# Remove default Nginx site sudo rm /etc/nginx/sites-enabled/default && \ # Link Nginx config sudo ln -s /opt/frappe/frappe-bench/config/nginx.conf /etc/nginx/conf.d/frappe.conf && \ # Link Supervisor config sudo ln -s /opt/frappe/frappe-bench/config/supervisor.conf /etc/supervisor/conf.d/frappe.conf && \ # Reload services sudo supervisorctl reread && \ sudo supervisorctl update && \ sudo systemctl restart nginx
If Nginx fails to start, it is often due to an undefined main log format. Adjust the configuration to use the default format.
Open the Frappe Nginx config:
nano /opt/frappe/frappe-bench/config/nginx.conf
Find the line (around line 101):
access_log /var/log/nginx/access.log main;
Change it to:
access_log /var/log/nginx/access.log;
Then restart Nginx:
sudo systemctl restart nginx
Install Certbot and generate an SSL certificate for your domain.
# Install Certbot via Snap sudo snap install core && sudo snap refresh core && \ sudo snap install --classic certbot && \ sudo ln -s /snap/bin/certbot /usr/bin/certbot && \ # Run the Bench SSL setup command using the full path sudo /home/frappe/.local/bin/bench setup lets-encrypt erp.yourdomain.com
Because we are using a restricted directory (/opt/frappe), verify that the home directory permissions are applied correctly so Nginx (running as www-data) can read the assets.
# Ensure the group 'frappe' has execute (traverse) permissions on the home directory sudo chmod 750 /opt/frappe
Finally, verify that all production services are running correctly:
# Check Supervisor status (Processes should be 'RUNNING') sudo supervisorctl status # Check Nginx status sudo systemctl status nginx
Your ERPNext instance should now be accessible at https://erp.yourdomain.com.
ERPNext comes with a standard print format builder, but for pixel-perfect invoices and reports, we recommend the Print Designer app. It offers a modern drag-and-drop interface.
Download the app code into your bench.
cd /opt/frappe/frappe-bench # Get the app (defaults to the latest compatible version) bench get-app print_designer
Note: If you are strictly version-binding, you can add
--branch version-16(or 15/14) to match your setup.
Install the application on your active site.
bench --site erp.yourdomain.com install-app print_designer
Restart the bench processes to load the new app, then confirm it is listed.
bench restart # Verify installation bench --site erp.yourdomain.com list-apps
1. Why use uv instead of standard pip?
uv is significantly faster and handles Python version management cleanly without interfering with the system Python system. It simplifies managing specific versions like Python 3.14.
2. I get a "Permission Denied" error on static files.
Ensure you ran the Phase 7 commands. Nginx runs as the www-data user, so it needs execute (traverse) permissions on the parent directories /opt and /opt/frappe to reach the static assets inside frappe-bench.
3. How do I update ERPNext? To update your apps (ERPNext, Frappe) and sites, run:
cd /opt/frappe/frappe-bench bench update
This pulls the latest code, runs database migrations, and builds the assets. To update the Bench CLI tool itself (e.g., if you see "A newer version of bench is available"), run:
uv tool upgrade frappe-bench
4. How do I fix "Unknown Column" database errors? This error occurs when the database schema is out of sync with the code (common after updates). To fix it, run the migration command manually:
cd /opt/frappe/frappe-bench bench --site erp.yourdomain.com migrate
5. How do I backup my site data? Regular backups are critical. Run this command to snapshot your database and files:
bench --site erp.yourdomain.com backup --with-files
Files are saved in sites/erp.yourdomain.com/private/backups.
6. How do I reset the Administrator password? If you lose access to the admin account, reset it via the terminal:
bench --site erp.yourdomain.com set-admin-password 'NewStrongPassword'
7. My scheduled jobs (emails) are not sending. First, validate that the scheduler is active:
bench --site erp.yourdomain.com enable-scheduler
If issues persist, check that the execution workers are running:
sudo supervisorctl status
8. Where can I find error logs?
If the site shows an "Internal Server Error", check the logs in frappe-bench/logs:
# Web request errors tail -f logs/web.error.log # Background job errors tail -f logs/worker.error.log
9. How do I enable Server Scripts? Server scripts allow you to add custom Python logic directly from the desk interface but are disabled by default. To enable them:
cd /opt/frappe/frappe-bench # Enable globally bench set-config -g server_script_enabled 1 # Enable for your specific site bench --site erp.yourdomain.com set-config server_script_enabled 1 # Restart services to apply changes bench restart
About the Author
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Related Blog Posts
Enjoyed this blog post? Check out these related posts!

Install ERPNext 16 on Windows (WSL): A Complete Development Guide in 2026
The Ultimate Guide to Building Frappe Apps on Windows with WSL 2, and VS Code
Read More...

Mastering ERPNext 16 Development: The Complete Guide to Building Custom Apps
Build, Test, Deploy: A Modern CI/CD Pipeline for ERPNext Developers
Read More...

How to Install ERPNext on Ubuntu with Docker
A Complete Guide to Deploying ERPNext with Docker Compose, Nginx, and SSL
Read More...

Add Client-Side Search to Your Reflex Blog with MiniSearch
How to Build Fast, Offline Search for Your Python Blog Using MiniSearch and Reflex
Read More...
On this page
Back to Blogs
Contact Me
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.