Need to upgrade?
├── Single minor version bump (e.g., v15.10 → v15.20)?
│ └── YES → Run `bench update` directly
├── Major version jump (e.g., v14 → v15)?
│ ├── Have custom apps?
│ │ ├── YES → Test on staging FIRST, check breaking changes
│ │ └── NO → Follow standard upgrade path
│ └── Multiple major versions (v14 → v16)?
│ └── ALWAYS upgrade one version at a time: v14 → v15 → v16
└── Production environment?
├── YES → ALWAYS test on staging clone first
└── NO → Proceed with standard upgrade
Pre-Upgrade Checklist
ALWAYS complete these steps before ANY major version upgrade:
Full backup — bench --site mysite backup --with-files
Test on staging — Clone production to a staging bench and test there first
Check breaking changes — Review the breaking changes section below
Audit custom apps — Run custom apps against new version's API changes
Patches are one-off data migration scripts that run during bench migrate. They are defined in each app's patches.txt file.
patches.txt Format [v14+]
[pre_model_sync]# Runs BEFORE schema sync — use for data prep
myapp.patches.v15_0.prepare_data_for_migration
[post_model_sync]# Runs AFTER schema sync — use for data that needs new schema
myapp.patches.v15_0.migrate_data_to_new_fields
Patch Execution Rules
Patches run in the order defined in patches.txt
Each patch runs exactly ONCE — tracked in the __patches table
To re-run a patch, append a date comment: myapp.patches.v15_0.fix #2025-03-20
# myapp/patches/v15_0/migrate_field_data.pyimport frappe
defexecute():
# ALWAYS reload if you need the NEW schema
frappe.reload_doc("module_name", "doctype", "doctype_name")
# Perform data migration
frappe.db.sql("""
UPDATE `tabSales Invoice`
SET new_field = old_field
WHERE old_field IS NOT NULL
""")
Debugging Stuck Patches
# Check which patches have run
bench --site mysite console
>>> frappe.db.sql("SELECT * FROM __patches WHERE patch LIKE '%stuck_patch%'")
# Remove a patch record to force re-run
>>> frappe.db.sql("DELETE FROM __patches WHERE patch = 'myapp.patches.v15_0.broken_patch'")
>>> frappe.db.commit()
# Then re-run migrate
bench --site mysite migrate
Rollback Procedure
Immediate Rollback (Within Hours)
# 1. Stop all processes
bench stop
# 2. Restore database from pre-upgrade backup
bench --site mysite restore /path/to/pre-upgrade-backup.sql.gz \
--with-public-files /path/to/files.tar \
--with-private-files /path/to/private-files.tar
# 3. Switch back to previous version branch
bench switch-to-branch version-14 frappe erpnext
# 4. Install old dependencies
bench setup requirements
# 5. Build old assets
bench build
# 6. Start bench
bench start # or: sudo bench restart (production)
Critical Rules for Rollback
ALWAYS keep pre-upgrade backups for at least 7 days
NEVER run bench migrate after restoring to old branch — schema is already correct
NEVER attempt rollback after users have created new data on the upgraded version
Frappe Packages: Moving Customizations Between Sites
Frappe Packages (v14+) are lightweight UI-built applications — bundles of Custom Module Defs distributed as .tar.gz tarballs. For Custom Fields, Property Setters, and DocPerms on standard DocTypes, use Fixtures instead.
Quick Reference: Package vs Fixtures vs App
Mechanism
Use When
CLI Command
Package
UI-built DocTypes, Scripts, Web Pages
UI only (Package Import/Release)
Fixtures
Custom Fields, Property Setters, DocPerms
bench --site mysite export-fixtures
Frappe App
Full development workflow, CI/CD, tests
bench get-app, bench install-app
Package Workflow (UI-Based)
Create a Package document → assign Custom Module Defs to it
Create a Package Release → exports to [bench]/sites/[site]/packages/ as [package]-[version].tar.gz
System migrates data like an app migration; use Force to overwrite existing files
Fixtures Workflow (CLI-Based)
# hooks.py — define what to export
fixtures = [
"Custom Field",
"Property Setter",
{"dt": "Client Script", "filters": [["module", "=", "My Module"]]}
]
# Export fixtures to JSON in your app
bench --site mysite export-fixtures --app myapp
# Fixtures auto-sync on: bench --site mysite migrate
NEVER use Packages to modify standard/core DocTypes — use a Frappe App with Fixtures.
See frappe-packages.md for the complete reference including decision trees, limitations, and best practices.
Custom App Compatibility Checks
Before upgrading, audit each custom app:
Check deprecated APIs — Search for removed functions listed in breaking changes
Check setup.py — Must migrate to pyproject.toml for v15+
Check Vue components — Must be Vue 3 compatible for v15+
Check patches.txt — Ensure patches use [pre_model_sync]/[post_model_sync] sections [v14+]
Check hooks.py — Verify no removed hooks are used
Run tests — bench --site test_site run-tests --app myapp
Decision Tree: In-Place vs Fresh Install
Choosing upgrade strategy:
├── Small site (< 10 GB database)?
│ └── In-place upgrade is usually fine
├── Large site (> 50 GB database)?
│ ├── Many custom apps? → Fresh install + data migration
│ └── Standard apps only? → In-place with extended downtime window
├── Skipping multiple versions (v13 → v15)?
│ └── ALWAYS fresh install — sequential upgrades are too risky
└── Critical production with zero-downtime requirement?
└── Fresh install on parallel server + DNS switch