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.
| Component | Role |
|---|---|
| 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
- Set up a production config file.
- Run Odoo behind an Nginx reverse proxy.
- Run it as a systemd service.
- Set workers for multiprocessing.
- 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.
- Module versioning and what triggers an upgrade
- The upgrade process: what
odoo-bin -udoes - Writing migration scripts for data changes
- Using
pre_init_hookandpost_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:
{
'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:
- Load existing database state (old version)
- Run
pre_migrate()scripts (if version changed) - Apply new field definitions (ALTER TABLE for new columns)
- Load XML data files marked with
noupdate="0" - Run
post_migrate()scripts - 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):
# 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.
Install and Upgrade Hooks
Hooks in __manifest__.py run Python functions at specific points during install/upgrade:
# __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
| Step | Action |
|---|---|
| 1 | Backup production database before any upgrade |
| 2 | Test upgrade on a copy of production data first |
| 3 | Put Odoo in maintenance mode (stop web workers) |
| 4 | Deploy new module code to the server |
| 5 | Run odoo-bin -u my_module --stop-after-init |
| 6 | Check logs for errors before restarting web workers |
| 7 | Start web workers and verify functionality |
| 8 | Clear browser cache / asset regeneration if needed |
- Bump the last two version numbers in
__manifest__.pyto trigger migration scripts - Put pre-migration data transforms in
pre_migrate.py; post-migration backfills inpost_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_modedo? - How do workers affect performance?
- How do you secure an Odoo production instance?
Related Topics
FAQ
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.
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.
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 ....

