Developing for Frappe and ERPNext requires a Linux environment due to its heavy reliance on Redis, MariaDB, and Unix-specific Python libraries. Historically, this forced Windows developers to use slow Virtual Machines or dual-boot their systems.
With WSL 2 (Windows Subsystem for Linux), you can now run a full Ubuntu environment directly on Windows with near-native performance. This allows you to write code in VS Code on Windows while your app runs in a real Linux environment just like your production server.
This guide covers setting up a high-performance local development environment for ERPNext Version 16 on Windows 10/11 using Ubuntu 24.04 LTS. If you are looking to deploy to production, check out our guide on Installing ERPNext on Ubuntu 24 or Deploying ERPNext with Docker.
We are building a "Hybrid" workflow:
Open PowerShell as Administrator and run:
wsl --install
If you already have WSL but need to upgrade to version 2 (mandatory for performance), refer to the official Microsoft WSL documentation:
wsl --set-default-version 2
Restart your computer if prompted.
Launch "Ubuntu" from your Start menu and create your UNIX root username and password when prompted.
Tip: When creating your username, avoid using
rootoradmin. A standard name likedavidordeveloperis perfect.
To make this workflow seamless, you need:
This allows you to open any folder inside Ubuntu by typing code . in your terminal.
From this point on, all commands are run inside your Ubuntu terminal.
We need the basic build tools, Git, and MariaDB database services.
sudo apt update && sudo apt upgrade -y sudo apt install -y git redis-server mariadb-server mariadb-client \ pkg-config libmariadb-dev gcc build-essential libssl-dev \ python3-dev python3-setuptools python3-pip xvfb libfontconfig nano curl
WSL 2 now supports systemd, which is required for MariaDB and Redis to run automatically. Check if it's active:
systemctl list-unit-files --type=service
If you get an error, edit /etc/wsl.conf and add:
[boot] systemd=true
Then restart WSL (wsl --shutdown in PowerShell).
ERPNext uses wkhtmltopdf (with patched Qt) to generate PDF reports. Since this package was dropped from Ubuntu 24 repositories, we install it manually. This ensures you can generate invoices and reports locally, a common requirement for ERP development.
# 1. Install libssl dependency (Required for legacy wkhtmltopdf) 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 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 missing dependencies sudo apt --fix-broken install -y
Developers often encounter issues with emoji support or strict mode in MariaDB. Let's configure it correctly from the start.
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 the service:
sudo systemctl restart mariadb
Running mysql_secure_installation is recommended even for local dev to avoid permission issues later.
sudo mariadb-secure-installation
Follow the prompts (Set root password, remove anonymous users, remove test db).
For a stable development environment, we use nvm (Node Version Manager) and uv (Python manager). This allows you to switch versions easily if you work on multiple projects.
We recommend installing Node using nvm (Node Version Manager) to manage versions easily. While modern Frappe setups (Version 16) perform best with Node 24, managing multiple versions is often necessary.
# Install NVM curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash # Close and reopen terminal, or source the config source ~/.bashrc # Install Node 24 nvm install 24 nvm use 24 nvm alias default 24 # Verify version (Should be v24.x.x) node -v # Install Yarn npm install -g yarn
We use uv to manage Python versions without messing with the system Python. uv is significantly faster than pip and handles virtual environments automatically.
# Install uv curl -LsSf https://astral.sh/uv/install.sh | sh source ~/.bashrc # Install Python 3.14 uv python install 3.14 # Install Bench CLI # Bench is the CLI tool for Frappe Framework: https://frappeframework.com/docs/user/en/bench uv tool install frappe-bench --python python3.14 # Confirm installation bench --version
Now we create the folder that will hold all your apps and sites.
cd ~ bench init frappe-bench --frappe-branch version-16 --python python3.14 cd frappe-bench
Now is the perfect time to open your project in VS Code on Windows.
code .
You will see VS Code open with a connection to WSL: Ubuntu. You can now edit files, use the integrated terminal, and use Git GUI tools directly.
In development, we map a site to localhost.
bench get-app --branch version-16 erpnext
bench new-site dev.localhost --admin-password 'admin' --mariadb-root-password 'YourRootPass'
bench --site dev.localhost install-app erpnext
This ensures any changes you make to DocTypes are saved to the file system (JSON files) so Git can track them.
bench --site dev.localhost set-config developer_mode 1
To start your development server, run:
bench start
Open your browser in Windows and go to: http://localhost:8000.
Now that your development environment is set up, let's create a custom app for your ERPNext modifications. The best practice is to treat each custom app as its own Git repository for easy versioning, collaboration, and deployment.
From your bench directory:
cd ~/frappe-bench # Create a new app scaffold (replace 'my_custom_app' with your app name; use lowercase + underscore) bench new-app my_custom_app
Follow the prompts:
you@example.com)This creates apps/my_custom_app/ with the Frappe app scaffold, including basic structure for doctypes, modules, and hooks.
Turn your app into a Git repo and push to GitHub:
cd apps/my_custom_app # Initialize Git git init git add . git commit -m "Initial app scaffold" # Create a new repository on GitHub (e.g., github.com/yourusername/my_custom_app) # Then add remote and push git remote add origin git@github.com:yourusername/my_custom_app.git git branch -M main git push -u origin main
Tip: Use SSH keys for GitHub authentication to avoid password prompts. Generate them with ssh-keygen and add the public key to your GitHub account.
# From bench root cd ~/frappe-bench # Install on your site bench --site dev.localhost install-app my_custom_app # Build frontend assets bench build # Apply database migrations bench --site dev.localhost migrate
Your custom app is now active on the site.
For UI-based development, enable developer mode:
# Edit site config nano sites/dev.localhost/site_config.json
Add this line (if not already present):
{ "developer_mode": 1 }
Restart your bench:
bench restart
In your browser at http://localhost:8000:
After creating in the UI, export to your app's code:
# From bench root bench --site dev.localhost export-json "Your DocType Name" \ apps/my_custom_app/my_custom_app/doctype/your_doctype/your_doctype.json
Commit the changes:
cd apps/my_custom_app git add . git commit -m "Add Your DocType" git push
Create .gitignore in your app repo:
__pycache__/ *.pyc *.pyo *.pyd .DS_Store .idea/ .vscode/ node_modules/ dist/ *.log env/ .env *.sqlite3 *.db
Critical: Never commit sites/*/site_config.json or sites/*/private/ as they contain database passwords and secrets. These files are outside your app repository (in the bench root), so they won't be included unless you mistakenly add them. If you version your entire bench directory, add sites/*/ to your bench-level .gitignore.
Create a feature branch:
git checkout -b feature/add-new-doctype
Make changes in apps/my_custom_app/ (add doctypes, Python code, JavaScript, etc.)
Test locally:
# Build assets if you changed frontend code bench build # Apply migrations bench --site dev.localhost migrate # Restart server bench restart
Commit and push:
git add . git commit -m "Implement new feature" git push origin feature/add-new-doctype
Create a Pull Request on GitHub for review.
For a real-world CI/CD setup, use GitHub Actions to automate testing and deployment. Create .github/workflows/ci.yml in your app repository.
If you are interested in automating Python scripts, check out our guide on Running Python Scripts with GitHub Actions.
name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest services: mariadb: image: mariadb:11.8 env: MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 redis: image: redis:alpine ports: - 6379:6379 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.14' - name: Setup Node uses: actions/setup-node@v4 with: node-version: '24' - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install Bench run: | uv tool install frappe-bench --python python3.14 # Add uv tools to path for subsequent steps echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Initialize Bench run: | bench init --skip-redis-config-generation --skip-assets --python python3.14 frappe-bench - name: Install ERPNext working-directory: frappe-bench run: | bench get-app --branch version-16 erpnext - name: Install Custom App working-directory: frappe-bench run: | # We symlink the checked-out code into the apps directory # This allows us to test the current commit without re-cloning mkdir -p apps ln -s $GITHUB_WORKSPACE apps/my_custom_app # Install the app dependency uv pip install --python env/bin/python -e apps/my_custom_app - name: Create Site working-directory: frappe-bench run: | bench new-site test_site \ --mariadb-root-password root \ --admin-password admin \ --no-mariadb-socket \ --db-host 127.0.0.1 \ --install-app erpnext - name: Install Custom App on Site working-directory: frappe-bench run: | bench --site test_site install-app my_custom_app - name: Run Tests working-directory: frappe-bench run: | bench --site test_site run-tests --app my_custom_app
This pipeline:
To deploy your app to a production server, add a deployment job that uses SSH. First, set up these additional secrets:
SSH_HOST: Your production server IP/hostnameSSH_USER: SSH usernameSSH_KEY: Private SSH key (add as a secret, not a variable)deploy: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' # Only deploy on main branch steps: - name: Deploy to Production uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} script: | cd /path/to/your/bench bench get-app my_custom_app https://github.com/yourusername/my_custom_app.git bench --site yoursite.com install-app my_custom_app bench --site yoursite.com migrate bench build bench restart
1. How do I access the MariaDB database from a Windows GUI client?
Tools like DBeaver or HeidiSQL installed on Windows can connect to MariaDB inside WSL. Use localhost as the host, port 3306, and the credentials you set in Phase 3.
2. Why is my disk IO slow?
Crucial: Always store your project files inside the Linux filesystem (/home/your_username/...). Do not use /mnt/c/Users/.... Cross-OS file system access is very slow.
3. How do I debug Python code?
In VS Code connected to WSL, install the Python extension. You can then add a launch.json configuration to attach the debugger to the running bench process or run specific scripts.
4. Can I use Docker instead? Yes, but Docker on Windows adds a layer of abstraction that can make debugging code harder. This "Metal on WSL" approach is often preferred for core backend development because the code runs directly in the OS you are interacting with.
5. What should I do if bench start fails to start the server?
Check if ports 8000 (web) and 3306 (DB) are free. Run sudo netstat -tlnp | grep :8000 to check. Also, ensure Redis and MariaDB services are running with sudo systemctl status redis-server and sudo systemctl status mariadb. Restart WSL if needed.
6. How do I update ERPNext or my custom app?
For ERPNext: bench update --pull (from bench root). For your app: Pull latest changes in your app repo, then bench --site dev.localhost migrate and bench build. Always backup your site first with bench --site dev.localhost backup.
7. What's the difference between code-first and UI-first DocType creation? UI-first is easier for beginners (design in browser, export JSON). Code-first is better for version control (write JSON/Python manually, then migrate). Use UI-first for prototyping, code-first for production features.
8. How do I handle multiple developers working on the same app? Use Git branches for features. Each developer should have their own WSL bench. For shared development, consider a central bench server or use Docker for consistent environments. Always pull before pushing and resolve conflicts carefully.
9. Why are my changes not showing up after bench build?
Clear browser cache (Ctrl+F5) and check if assets are built correctly. Run bench clear-cache and restart the bench. Ensure your files are in the correct app directory and not overridden by site customizations.
10. How do I migrate from a Docker-based setup to WSL?
Create a backup of your Docker site using bench --site yoursite backup --with-files. Copy the backup files (SQL and public/private files) to your WSL instance. Create a new site in WSL, then restore using bench --site newsite restore /path/to/backup.sql.gz --with-public-files /path/to/public.tar --with-private-files /path/to/private.tar. Finally, install your custom apps and run bench migrate.
11. What are some performance tips for development?
Use bench start --concurrency 1 for lighter resource usage. Disable unnecessary apps on your dev site. Keep your WSL distro updated and avoid running too many services simultaneously. For large projects, consider increasing WSL memory allocation in .wslconfig.
12. How do I secure my development environment? Never commit secrets to Git. Use strong passwords for MariaDB. Limit WSL port exposure if needed. For production-like security, consider using separate VMs or cloud instances instead of local WSL for sensitive data.
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!

How to Install ERPNext on Ubuntu 24 Using Bench
A manual, production-ready guide using UV and Python 3.14
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...

Run Python Scripts for Free with GitHub Actions: A Complete Guide
Schedule and Automate Python Scripts Without Managing Servers or Cloud Bills
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.