Advertisement
⚡ Advanced Topics

Odoo Deployment and Upgrades – Production Best Practices

Deploying and upgrading Odoo modules in production requires care: the wrong approach can corrupt data or leave the system in an inconsistent state. The key tools are the version field in __manifest__.py, migration scripts, and install/upgrade hooks that run before and after module loading.

⏱️ 22 min 🎯 Advanced 📅 Updated 2026

Deploying Odoo to Production

Running Odoo for real is very different from dev mode. Production needs a proper process setup, a reverse proxy, workers tuned to your load, and — above all — backups and security hardening.

ComponentRole
Reverse proxy (Nginx)HTTPS, static files, buffering
Workers (multi-process)handle concurrent requests
PostgreSQL (tuned/separate)the data — back it up
Process manager (systemd)keep Odoo running, restart on crash
# key production settings in odoo.conf
workers = 4              # >0 enables multiprocessing (not dev mode)
proxy_mode = True        # trust the Nginx reverse proxy
admin_passwd = <strong>  # protect the database manager!
list_db = False          # hide the database list

Dev vs production is a real shift: in dev you run a single process with --dev=all; production sets workers > 0 to run multiple processes for concurrency, disables dev auto-reload, and runs behind Nginx for HTTPS and to serve static assets efficiently. Configure proxy_mode = True so Odoo trusts the proxy's headers. Security hardening is non-negotiable: set a strong admin_passwd (it guards the database-manager where anyone could otherwise create/drop/duplicate databases), set list_db = False to hide database names, always serve over HTTPS, and keep the server and Odoo patched. Backups are the thing you must not skip: automate regular PostgreSQL dumps and filestore backups (attachments live on disk, not in the DB), and test that you can actually restore them. Upgrades: test on a staging copy first — module and version upgrades can break customizations. Managed hosting (Odoo.sh) handles much of this for you at the cost of some control; self-hosting gives control but shifts all this responsibility onto you.

🏋️ Practical Exercise

  1. Set up a production config file.
  2. Run Odoo behind an Nginx reverse proxy.
  3. Run it as a systemd service.
  4. Set workers for multiprocessing.
  5. Enable HTTPS.

🔥 Challenge Exercise

Outline a production deployment: Odoo behind an Nginx reverse proxy with HTTPS, running as a systemd service with multiple workers, a dedicated PostgreSQL, and a secured config (proxy_mode, admin password, filtered databases). Explain why workers and a reverse proxy matter in production.

What you'll learn:
  • Module versioning and what triggers an upgrade
  • The upgrade process: what odoo-bin -u does
  • Writing migration scripts for data changes
  • Using pre_init_hook and post_init_hook
  • Production deployment checklist

Module Versioning

The version field in __manifest__.py controls when migration scripts run. Use the format major.minor.patch.module_version where the first 3 numbers match the Odoo version:

Python
{
    'name': 'Library Management',
    'version': '19.0.1.0.1',  # Odoo 19, module version 1.0.1
    # When you change module_version (last 2 numbers),
    # Odoo will run migration scripts on next -u
}

When you run odoo-bin -u my_module, Odoo compares the installed version with the manifest version. If they differ, it runs migration scripts for all intermediate versions before applying the new code.

The Upgrade Process

Running odoo-bin -u my_module does the following in order:

  1. Load existing database state (old version)
  2. Run pre_migrate() scripts (if version changed)
  3. Apply new field definitions (ALTER TABLE for new columns)
  4. Load XML data files marked with noupdate="0"
  5. Run post_migrate() scripts
  6. Run post_init_hook (if first install) or upgrade callbacks

XML data files with noupdate="1" are loaded only on first install and never again on upgrade — use this for default data that users may have modified.

Migration Scripts

Migration scripts handle data transformations when your module's data model changes. Place them in migrations/19.0.1.0.1/ (version-specific folder):

Python
# migrations/19.0.1.0.1/pre_migrate.py
# Runs BEFORE new field definitions are applied
def migrate(cr, version):
    """Rename column before ORM recreates it."""
    if not version:
        return  # first install, skip
    cr.execute("""
        ALTER TABLE library_book
        RENAME COLUMN old_field_name TO new_field_name
    """)

# migrations/19.0.1.0.1/post_migrate.py
# Runs AFTER new field definitions and XML data loading
def migrate(cr, version):
    """Backfill new column from existing data."""
    if not version:
        return
    cr.execute("""
        UPDATE library_book
        SET full_title = title || ' by ' || author
        WHERE full_title IS NULL
    """)

Migration scripts receive the database cursor (cr), not the ORM environment — use raw SQL. The version argument is the previously installed version. If version is None, this is a fresh install.

Advertisement

Install and Upgrade Hooks

Hooks in __manifest__.py run Python functions at specific points during install/upgrade:

Python
# __manifest__.py
{
    'name': 'Library Management',
    'version': '19.0.1.0.0',
    'pre_init_hook': 'pre_init_hook',
    'post_init_hook': 'post_init_hook',
    'post_load': 'post_load',
}

# __init__.py
def pre_init_hook(env):
    """Runs before module tables are created.
    Use for checks or pre-setup only — tables don't exist yet.
    """
    pass

def post_init_hook(env):
    """Runs after first install only.
    Tables exist; create default records here.
    """
    env['library.category'].create({'name': 'General'})

def post_load():
    """Runs after module code is loaded but before install.
    Use for monkey-patching third-party libraries.
    """
    pass

Production Deployment Checklist

StepAction
1Backup production database before any upgrade
2Test upgrade on a copy of production data first
3Put Odoo in maintenance mode (stop web workers)
4Deploy new module code to the server
5Run odoo-bin -u my_module --stop-after-init
6Check logs for errors before restarting web workers
7Start web workers and verify functionality
8Clear browser cache / asset regeneration if needed
Key takeaways:
  • Bump the last two version numbers in __manifest__.py to trigger migration scripts
  • Put pre-migration data transforms in pre_migrate.py; post-migration backfills in post_migrate.py
  • Always backup and test on a production data copy before upgrading live
  • Use noupdate="1" on XML records that users may have modified

Interview Questions

  • How do you deploy Odoo to production?
  • What is the role of a reverse proxy like Nginx?
  • What does proxy_mode do?
  • How do workers affect performance?
  • How do you secure an Odoo production instance?

FAQ

What happens if a migration script fails? +

Odoo wraps the upgrade in a database transaction. If a migration script raises an exception, the transaction is rolled back and the upgrade fails — your database remains in the pre-upgrade state. Fix the script and re-run. This is why testing on a copy first is critical: a failing migration on production could require a database restore.

When should I use a migration script vs a post_init_hook? +

post_init_hook runs only on the first install and uses the ORM environment. Migration scripts run on every version bump and use raw SQL (no ORM). Use hooks for initial data seeding after install. Use migration scripts for structural changes (column renames, data transformations) when upgrading from a previous version.

How do I handle removing a field safely? +

To remove a field: (1) in the new version, remove the field from your model Python class; (2) in the pre_migrate script, optionally save any data you need before the column is dropped; (3) Odoo does NOT automatically drop old columns — the column stays in PostgreSQL but becomes invisible to the ORM. If you need to clean up, add a post_migrate script that runs ALTER TABLE ... DROP COLUMN ....