Revision · February 2026

Odoo Deployment Guide

Made with ❤️ by Emad Felaya — A complete guide from zero to production

Ubuntu 24.04 LTS PostgreSQL 16 Python 3.10+ Nginx + SSL Enterprise & Community Version 19.0
⚡ Odoo 19 — Key Changes from v18

Before installing, know what changed. These affect configuration, custom modules, and daily operations:

AreaWhat ChangedAction Required
Networklongpolling_portgevent_port, xmlrpc_porthttp_port, xmlrpc_interfacehttp_interfaceUpdate odoo.conf parameter names
Controllerstype='json'type='jsonrpc'Update all custom JSON controllers
ORMname_get() removed, read_group() deprecatedUse display_name field, _read_group()
Securityres.groups restructured → privilege-basedCheck custom group references
UoMuom.category model removedUpdate any UoM category references
Importsxlsxwriter, registry import paths changedUpdate import statements
PayrollBatches renamed to "Pay Runs", work entries use durationReview payroll customizations
ExpensesExpense reports removed entirelyUse list view submit/approve flow
APINew JSON-2 API at /json/2/Use for new external integrations
AIBuilt-in AI agent system (search, fields, server actions)Configure in Settings → AI
Python APIself._contextself.env.context, self._uidself.env.uidUpdate all custom module references
Groupsusersuser_ids, groups_idgroup_idsUpdate field references in code and XML
Partnersres.partner.title model removed, mobile field removed from res.partnerCheck custom module dependencies
For custom module migration, run grep -rn "name_get\|type='json'\|longpolling_port\|xmlrpc_port\|xmlrpc_interface\|read_group(\|self\._context\|self\._uid\|_apply_ir_rules\|category_id\|\.users\b" /opt/odoo/custom-addons/ to find code that needs updating.
⚠️ CRITICAL: PostgreSQL 13.0+ Required
Odoo 19 will NOT work with PostgreSQL 12 or earlier due to enhanced JSONB operations. Verify your version before installation: psql --version. This guide installs PostgreSQL 16 (recommended).
🚀 Part 1: Installation
Hardware Requirements

Odoo uses a multi-process architecture. Each worker is a separate OS process handling HTTP requests (~150–300 MB RAM each). The formula (CPU × 2) + 1 optimizes for I/O-bound workloads where workers spend most time waiting on database queries.

ScaleActive Users~ConcurrentCPURAMStorageWorkers
Small1–50~82 cores4 GB128 GB SSD5
Medium50–200~304 cores8 GB256 GB SSD9
Large200–500~758 cores16 GB512 GB NVMe17
Enterprise500+100+16+ cores32+ GB1+ TB NVMe33+

Active users = accounts with login access (billed by Odoo). Concurrent = users making requests at the same moment (typically 10–15% of active). Each worker serves ~6 concurrent users, so 17 workers handle ~100 simultaneous sessions.

Storage planning: Database grows ~1–2 GB per 10K records. Filestore (attachments/images) depends on usage. Reserve 3–7× DB size for backup retention.

Memory formula: Base Odoo ~1GB + (~200MB × workers) + PostgreSQL shared_buffers (25% RAM). Example for 16GB server: 1GB base + 3.4GB (17 workers) + 4GB PostgreSQL = ~8.4GB, leaving headroom for OS cache.

1 System Preparation

Start with a clean Ubuntu 24.04 LTS. Proper locale prevents PostgreSQL collation issues with Arabic/multilingual data.

# Update all packages
sudo apt update && sudo apt upgrade -y

# Set locale (critical for PostgreSQL text sorting)
sudo localectl set-locale LANG=en_US.UTF-8
sudo timedatectl set-timezone Africa/Cairo

# Essential tools
sudo apt install -y git curl wget nano htop iotop net-tools \
    ufw fail2ban unzip software-properties-common
htop monitors CPU/RAM per process, iotop shows disk I/O — essential for diagnosing slow workers.
2 PostgreSQL 16

Odoo 19 requires PostgreSQL 13.0+ minimum (v16 recommended) for JSONB operations and query optimizations introduced in PostgreSQL 13+. The odoo user gets CREATEDB only — no superuser access (least privilege).

sudo apt install -y postgresql-16 postgresql-client-16 postgresql-contrib-16 \
    postgresql-16-pgvector    # Required for Odoo 19 AI features (vector embeddings)
sudo systemctl enable --now postgresql

# Create database user (createdb only, no superuser)
sudo -u postgres createuser --createdb --no-createrole --no-superuser odoo

# Install extensions in template1 (inherited by all new databases automatically)
# Then transfer ownership to odoo so it can ALTER FUNCTION at startup without superuser
sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS unaccent;"
sudo -u postgres psql -d template1 -c "ALTER FUNCTION unaccent(text) OWNER TO odoo;"
sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS vector;"

# Verify
sudo -u postgres psql -c "\du" | grep odoo
sudo -u postgres psql -c "SELECT version();"
pgvector is a PostgreSQL extension for vector similarity search. Odoo 19 uses it for AI embeddings (ai_embedding table). Without it, database restores will fail with extension "vector" is not available errors.
3 System Dependencies

Odoo compiles Python C extensions for XML parsing, image processing, LDAP authentication, and PDF generation. The patched wkhtmltopdf is a frequent source of issues — never skip it.

# Python build + library dependencies
sudo apt install -y python3-pip python3-dev python3-venv python3-wheel \
    build-essential libssl-dev libffi-dev \
    libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libpq-dev \
    libjpeg-dev libfreetype6-dev zlib1g-dev libcups2-dev \
    nodejs npm node-less \
    xfonts-75dpi xfonts-base fontconfig fonts-dejavu fonts-liberation

# Patched wkhtmltopdf (REQUIRED for PDF headers/footers/page numbers)
# Note: The Jammy (22.04) package works on Noble (24.04) via backward compat.
# If dependency errors occur, install with: sudo dpkg -i --force-depends ... && sudo apt -f install
cd /tmp
wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb
sudo apt install -y ./wkhtmltox_0.12.6.1-3.jammy_amd64.deb
rm wkhtmltox_0.12.6.1-3.jammy_amd64.deb
wkhtmltopdf --version  # Must show 0.12.6.1 with patched qt

# Create Odoo system user (required for both Community and Enterprise)
sudo useradd -m -d /opt/odoo -U -r -s /bin/bash odoo
Common mistake: apt install wkhtmltopdf installs an unpatched build — PDF reports will be missing headers, footers, and page numbers. Always use the GitHub release.
4 GitHub SSH Access Enterprise Only

Enterprise repository is private. Your GitHub account must be linked to an active Odoo subscription. Community-only installations: skip this step entirely — go straight to Step 5.

# Generate Ed25519 key (stronger + shorter than RSA)
sudo -u odoo ssh-keygen -t ed25519 -C "odoo@$(hostname)" -f /opt/odoo/.ssh/id_ed25519 -N ""

# Display public key — add this to GitHub → Settings → SSH Keys
sudo cat /opt/odoo/.ssh/id_ed25519.pub

# Test connection (accept host key automatically)
sudo -u odoo ssh -T git@github.com -o StrictHostKeyChecking=accept-new
# Expected: "Hi username! You've successfully authenticated..."
5 Clone Source Code

--depth 1 clones only the latest commit, saving ~2 GB of git history. Choose Community (free, LGPL) or Enterprise (paid subscription, includes Community).

# Create directory structure
sudo mkdir -p /opt/odoo/{custom-addons,odoo-server,enterprise,backups,scripts}
sudo chown -R odoo:odoo /opt/odoo
touch /opt/odoo/custom-addons/__init__.py

sudo su - odoo

# ═══ Community (public — always required for both editions) ═══
git clone https://github.com/odoo/odoo.git --depth 1 --branch 19.0 /opt/odoo/odoo-server

# ═══ Enterprise (private — requires SSH key + active subscription) ═══
# Skip this line for Community-only installations
git clone git@github.com:odoo/enterprise.git --depth 1 --branch 19.0 /opt/odoo/enterprise

# Verify
ls /opt/odoo/odoo-server/odoo-bin && echo "Community OK"
ls /opt/odoo/enterprise/web_enterprise 2>/dev/null && echo "Enterprise OK" || echo "Community-only (no enterprise)"

exit
Community vs Enterprise: Community includes CRM, Sales, Inventory, Manufacturing, Accounting, Website, and many more — fully functional. Enterprise adds Studio, multi-company, payroll localization, quality control, planning, helpdesk, and additional themes. Both use the same Community codebase as the foundation.
6 Python Virtual Environment

Virtual environments isolate Odoo's dependencies from the system Python, preventing conflicts during OS upgrades and making rollbacks simple.

sudo -u odoo python3 -m venv /opt/odoo/odoo-venv

sudo su - odoo
source /opt/odoo/odoo-venv/bin/activate

pip install --upgrade pip wheel setuptools
pip install -r /opt/odoo/odoo-server/requirements.txt

# Recommended extras
pip install watchdog       # Auto-reload during development
pip install phonenumbers   # Phone number formatting
pip install num2words      # Amount in words on invoices
pip install pyopenssl      # SSL support
pip install python-barcode # Barcode generation

# Verify key dependencies are installed
python -c "import lxml, PIL, psycopg2, babel, dateutil; print('All core dependencies OK')"

deactivate && exit
7 Configuration

This configuration is production-hardened: single database, no database manager, localhost-only binding (Nginx handles external access), and worker-based multiprocessing.

sudo mkdir -p /etc/odoo /var/log/odoo && sudo chown odoo:odoo /var/log/odoo

sudo tee /etc/odoo/odoo.conf > /dev/null << 'EOF'
[options]
;; Paths — Enterprise (includes enterprise modules):
addons_path = /opt/odoo/odoo-server/addons,/opt/odoo/enterprise,/opt/odoo/custom-addons
;; Community-only (remove enterprise path):
;addons_path = /opt/odoo/odoo-server/addons,/opt/odoo/custom-addons
data_dir = /opt/odoo/.local/share/Odoo

;; Database — Unix socket (faster than TCP), single-database mode
;; db_host, db_port, db_password: omitted = Unix socket (recommended for local PostgreSQL)
db_user = odoo
db_name = production_db
db_maxconn = 32
list_db = False

;; Security — database manager master password (NOT the admin user login!)
admin_passwd = CHANGE_THIS_STRONG_PASSWORD

;; Performance — adjust workers to your CPU count
workers = 17
max_cron_threads = 2
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limit_time_real = 1200
limit_request = 8192

;; Network — bind to localhost only, Nginx handles external
http_port = 8069
gevent_port = 8072
http_interface = 127.0.0.1
proxy_mode = True

;; Logging
logfile = /var/log/odoo/odoo.log
log_level = warn
log_handler = :WARNING,odoo.http:WARNING,werkzeug:WARNING

;; ════════════════════════════════════════════════════════════
;; Odoo 19 Specific Settings
;; ════════════════════════════════════════════════════════════

;; Database Configuration (v19 optimized)
db_template = template0
unaccent = True

;; OPTIONAL: AI Features (uncomment if using Odoo AI)
;ai_provider = openai
;ai_api_key = your_api_key_here

;; OPTIONAL: ESG Module (uncomment if using sustainability tracking)
;esg_emission_factor_db = ademe
EOF

sudo chown odoo:odoo /etc/odoo/odoo.conf && sudo chmod 640 /etc/odoo/odoo.conf

# ── Hash the admin_passwd (replace MySecurePassword with your password) ──
HASHED=$(sudo -u odoo /opt/odoo/odoo-venv/bin/python3 -c "
from passlib.context import CryptContext
print(CryptContext(schemes=['pbkdf2_sha512']).hash('MySecurePassword'))
")
sudo sed -i "s|^admin_passwd = .*|admin_passwd = $HASHED|" /etc/odoo/odoo.conf
echo "Password hashed and saved to odoo.conf"
Odoo 19: Use http_port (not xmlrpc_port), http_interface (not xmlrpc_interface), and gevent_port (not longpolling_port). The old parameter names are silently ignored.
Session security: Secure cookies (SameSite, Secure flag) are handled at the Nginx/proxy level via HTTPS, not in odoo.conf. JSON-RPC is always enabled on the main HTTP port — no separate config needed.
Community-only? Comment out the Enterprise addons_path line and uncomment the Community-only line. That's the only change needed — everything else (workers, ports, logging) stays the same.
8 Systemd Service

Systemd handles automatic startup, crash recovery (RestartSec=5), and process supervision. PrivateTmp and NoNewPrivileges add security hardening. For Community-only, change the Description to "Odoo 19 Community".

sudo tee /etc/systemd/system/odoo.service > /dev/null << 'EOF'
[Unit]
Description=Odoo 19 Enterprise
Documentation=https://www.odoo.com/documentation/19.0/
Requires=postgresql.service
After=network.target postgresql.service

[Service]
Type=simple
SyslogIdentifier=odoo
User=odoo
Group=odoo
ExecStart=/opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin -c /etc/odoo/odoo.conf
WorkingDirectory=/opt/odoo/odoo-server
StandardOutput=journal+console
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload && sudo systemctl enable --now odoo
sudo systemctl status odoo

Service File Explanation

Directive Purpose
Requires=postgresql.service Ensures PostgreSQL is running before starting Odoo
After=network.target Starts after network target is reached
Restart=on-failure Auto-restart if service crashes
RestartSec=5 Wait 5 seconds before restarting
NoNewPrivileges=true Security: prevents privilege escalation
PrivateTmp=true Security: isolated /tmp directory
ProtectSystem=full Security: read-only /usr and /boot
ProtectHome=read-only Security: read-only access to home directories
9 Nginx + SSL

Nginx serves as reverse proxy, SSL terminator, and static file cache. It also enforces upload limits and timeouts for large imports.

sudo apt install -y nginx

sudo tee /etc/nginx/sites-available/odoo > /dev/null << 'EOF'
upstream odoo { server 127.0.0.1:8069 fail_timeout=0; }
upstream odoochat { server 127.0.0.1:8072 fail_timeout=0; }

server {
    listen 80;
    server_name erp.example.com;

    client_max_body_size 200M;   # Allow large file uploads
    proxy_read_timeout 720s;     # Long reports/exports
    proxy_connect_timeout 720s;
    proxy_send_timeout 720s;

    # Pass real client info to Odoo
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Main application
    location / { proxy_pass http://odoo; proxy_redirect off; }

    # Longpolling/WebSocket for real-time notifications
    # Odoo 19: /websocket is primary; /longpolling kept for backward compatibility
    location /longpolling { proxy_pass http://odoochat; }

    # WebSocket (Odoo 19 live features — discuss, notifications, bus)
    location /websocket {
        proxy_pass http://odoochat;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Cache static assets for 90 days
    location ~* /web/static/ {
        proxy_pass http://odoo;
        expires 90d;
        add_header Cache-Control "public, immutable";
    }

    # Gzip compression
    gzip on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
}
EOF

sudo ln -sf /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

# SSL with Let's Encrypt (auto-configures Nginx for HTTPS)
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d erp.example.com
sudo certbot renew --dry-run  # Verify auto-renewal works
Certbot modifies your Nginx config to add HTTPS redirect and SSL settings. Certificates auto-renew via systemd timer — no cron needed.
10 Firewall & Fail2ban

Block direct Odoo access — all traffic must flow through Nginx for SSL encryption and access logging.

# Enable firewall
sudo ufw enable
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw deny 8069/tcp comment 'Block direct Odoo'
sudo ufw deny 8072/tcp comment 'Block direct websocket/longpolling'
sudo ufw status verbose

# Fail2ban: protect against login brute-force
sudo systemctl enable --now fail2ban

sudo tee /etc/fail2ban/filter.d/odoo.conf > /dev/null << 'EOF'
[Definition]
failregex = Login failed for db:.* login:.* from 
ignoreregex =
EOF

sudo tee /etc/fail2ban/jail.d/odoo.conf > /dev/null << 'EOF'
[odoo]
enabled = true
port = http,https
filter = odoo
logpath = /var/log/odoo/odoo.log
maxretry = 5
bantime = 3600
findtime = 600
EOF

sudo systemctl restart fail2ban
🐘 PostgreSQL Tuning

Default PostgreSQL settings are for a small server with 128MB RAM. Odoo benefits dramatically from proper memory allocation and SSD-optimized settings. jit = off is critical — JIT compilation is designed for analytical queries and actually slows Odoo's transactional workload.

⚠️ CRITICAL: Match values to your actual RAM! Applying settings for a 16GB server on a 2GB machine will crash PostgreSQL with "Cannot allocate memory". Check your RAM first: free -h, then use the matching column from the table below.
# ═══ Check your RAM first ═══
free -h

# ═══ Apply settings (ADJUST VALUES to match your RAM — see table below) ═══
sudo -u postgres psql << 'EOF'
-- ══ Memory (choose values from the RAM table below) ══
ALTER SYSTEM SET shared_buffers = '512MB';           -- 25% RAM (example: 2GB server)
ALTER SYSTEM SET effective_cache_size = '1536MB';    -- 75% RAM
ALTER SYSTEM SET work_mem = '4MB';                   -- Per-sort operation
ALTER SYSTEM SET maintenance_work_mem = '128MB';     -- VACUUM, CREATE INDEX

-- ══ WAL & Checkpoints (same for all sizes) ══
ALTER SYSTEM SET checkpoint_timeout = '15min';
ALTER SYSTEM SET checkpoint_completion_target = 0.9;
ALTER SYSTEM SET min_wal_size = '256MB';
ALTER SYSTEM SET max_wal_size = '1GB';

-- ══ SSD Optimization (same for all sizes) ══
ALTER SYSTEM SET random_page_cost = 1.1;
ALTER SYSTEM SET effective_io_concurrency = 200;
ALTER SYSTEM SET jit = off;

-- ══ Connections ══
ALTER SYSTEM SET max_connections = 50;               -- Adjust: 50 (2GB) / 100 (8GB) / 150 (16GB+)
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- Log slow queries (>1s)

-- ══ Odoo 19 Specific ══
-- V19: Prevent runaway queries
ALTER SYSTEM SET statement_timeout = '300000';       -- 5 minutes (in milliseconds)
ALTER SYSTEM SET idle_in_transaction_session_timeout = '600000';  -- 10 minutes

-- V19: Large report/BOM handling (for manufacturing operations)
ALTER SYSTEM SET temp_file_limit = '5242880';        -- 5GB in KB (adjust for larger servers)

-- V19: Parallel query support (adjust to CPU cores)
ALTER SYSTEM SET max_parallel_workers_per_gather = 1;  -- 1 (2GB) / 2 (8GB) / 4 (16GB+)
ALTER SYSTEM SET max_parallel_workers = 2;             -- 2 (2GB) / 4 (8GB) / 8 (16GB+)
ALTER SYSTEM SET max_parallel_maintenance_workers = 1; -- 1 (2GB) / 2 (8GB) / 4 (16GB+)

-- V19: Egyptian/Arabic locale support
-- NOTE: lc_* settings are set at initdb time and cannot be changed via ALTER SYSTEM.
-- Ensure correct locale when creating the PostgreSQL cluster:
--   sudo pg_createcluster --locale=en_US.UTF-8 16 main
ALTER SYSTEM SET timezone = 'Africa/Cairo';

SELECT pg_reload_conf();
EOF
sudo systemctl restart postgresql

Settings by RAM Size

SettingPurpose2GB4GB8GB16GB32GB64GB
shared_buffers25% RAM — PostgreSQL cache512MB1GB2GB4GB8GB16GB
effective_cache_size75% RAM — OS + PG cache hint1.5GB3GB6GB12GB24GB48GB
work_memPer-sort/hash operation4MB8MB32MB64MB128MB256MB
maintenance_work_memVACUUM, CREATE INDEX128MB256MB256MB512MB1GB2GB
max_connectionsTotal connection slots5075100150200300
Always set jit = off and random_page_cost = 1.1 (for SSD). These two settings alone can improve Odoo query performance by 20-40%.

Verify Applied Settings

sudo -u postgres psql -c "
SELECT name, setting, unit FROM pg_settings 
WHERE name IN ('shared_buffers','effective_cache_size','work_mem','jit','random_page_cost','max_connections','log_min_duration_statement');"
✅ Post-Install Verification

Run these checks to confirm everything is working before going live:

# 1. All services running
sudo systemctl status postgresql odoo nginx --no-pager

# 2. Ports listening
ss -tlnp | grep -E "(5432|8069|8072|80|443)"
#   5432=PostgreSQL  8069=Odoo  8072=Longpolling  80/443=Nginx

# 3. No errors in Odoo log
sudo tail -50 /var/log/odoo/odoo.log | grep -iE "(error|critical)"

# 4. HTTP responses
curl -s -o /dev/null -w "Odoo direct: %{http_code}\n" http://localhost:8069/web/login
curl -s -o /dev/null -w "Via Nginx:   %{http_code}\n" https://erp.example.com/web/login

# 5. Database size
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size('production_db'));"

# 6. Worker count
ps aux | grep "odoo-bin" | grep -v grep | wc -l
# Should be: workers + 1 (main) + max_cron_threads

# 7. Edition check
sudo -u postgres psql -d production_db -t -c "SELECT CASE WHEN EXISTS(SELECT 1 FROM ir_module_module WHERE name='web_enterprise' AND state='installed') THEN 'Enterprise' ELSE 'Community' END;"
🗄️ First Database Setup

After installation, create the production database and install initial modules. The -i flag installs modules. Odoo 19 no longer loads demo data by default — the --without-demo flag is included for explicitness and backward compatibility.

# Create database and install base modules
sudo systemctl stop odoo
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo.conf -d production_db \
    -i base,web,mail,contacts --without-demo=all --stop-after-init

# Install business modules (example for manufacturing company)
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo.conf -d production_db \
    -i sale_management,purchase,stock,mrp,hr,hr_holidays,account \
    --stop-after-init

sudo systemctl start odoo

After first login (admin/admin), immediately change the admin password via Settings → Users → Administrator.

Module install order matters. Install base first, then business modules. If you install all at once with -i, Odoo resolves dependencies automatically, but it's slower than sequential installs for large module sets.
🗃️ Database Manager (Web UI)

Odoo ships with a web-based Database Manager for creating, duplicating, backing up, and deleting databases through the browser. It's disabled by default in this guide for security — enable it only when needed, then disable it again.

Enable Database Manager

Two config changes are required:

# Edit the config for the target instance
sudo nano /etc/odoo/odoo.conf   # or odoo-community.conf, odoo-staging.conf, etc.

# 1. Change list_db to True
list_db = True

# 2. Set db_name to False (allows listing all databases)
db_name = False

# 3. IMPORTANT: Remove or comment out db_host entirely
#    If db_host is set (even to False), Odoo uses TCP which requires a password.
#    Without db_host, Odoo uses Unix socket → peer authentication → no password needed.
; db_host = False

# Restart the service
sudo systemctl restart odoo          # or odoo-community, odoo-staging, etc.
⚠️ db_host pitfall: If db_host is present in the config (even set to False), Odoo connects via TCP on port 5432, which triggers fe_sendauth: no password supplied. Removing the line entirely forces Unix socket connection with peer authentication — no password needed.

Access the Database Manager at:

https://your-domain.com/web/database/manager

Master Password (admin_passwd)

The admin_passwd in odoo.conf protects the Database Manager. Always hash it — never store plain text:

# Generate hashed password
python3 -c "from passlib.hash import pbkdf2_sha512; print(pbkdf2_sha512.using(rounds=600000).hash('YOUR_STRONG_PASSWORD'))"

# Put the output in odoo.conf:
# admin_passwd = $pbkdf2-sha512$600000$...

Disable After Use

When done, immediately re-secure the instance:

# Edit config
sudo nano /etc/odoo/odoo.conf

# Restore production settings:
list_db = False
db_name = production_db       # Lock to single database
; db_host stays removed/commented

# Restart
sudo systemctl restart odoo
🔒 Security: Never leave list_db = True on a production server. Anyone who discovers the URL can see all databases, and with the master password can create, duplicate, or delete databases.
For multi-instance setups (staging, community, v18), each instance has its own config file. Enable Database Manager per-instance by editing the specific config (/etc/odoo/odoo-community.conf, etc.) and restarting only that service.
🖥️ Part 2: Multi-Instance Setup
🖥️ Multi-Instance Setup

Run multiple Odoo instances alongside production on the same server using different ports, data directories, and systemd services. Common scenarios: staging for testing, or a Community edition instance for non-licensed workloads.

# Create data directories for additional instances
sudo -u odoo mkdir -p /opt/odoo/.local/share/Odoo-Staging
sudo -u odoo mkdir -p /opt/odoo/.local/share/Odoo-Community
sudo -u odoo mkdir -p /opt/odoo/.local/share/Odoo18
Staging Instance (Enterprise)
sudo tee /etc/odoo/odoo-staging.conf > /dev/null << 'EOF'
[options]
addons_path = /opt/odoo/odoo-server/addons,/opt/odoo/enterprise,/opt/odoo/custom-addons
data_dir = /opt/odoo/.local/share/Odoo-Staging
db_name = staging_db
db_user = odoo
admin_passwd = STAGING_PASSWORD
http_port = 8079
gevent_port = 8082
http_interface = 127.0.0.1
proxy_mode = True
list_db = False
workers = 2
max_cron_threads = 1
limit_memory_hard = 2147483648
logfile = /var/log/odoo/odoo-staging.log
log_level = info
EOF
sudo chown odoo:odoo /etc/odoo/odoo-staging.conf && sudo chmod 640 /etc/odoo/odoo-staging.conf
Community Instance (No Enterprise License)

Useful for dev/testing, public-facing websites, or departments that don't need Enterprise modules. Uses the same Community codebase — just exclude the enterprise addons_path.

sudo tee /etc/odoo/odoo-community.conf > /dev/null << 'EOF'
[options]
;; Community-only: NO enterprise path
addons_path = /opt/odoo/odoo-server/addons,/opt/odoo/custom-addons
data_dir = /opt/odoo/.local/share/Odoo-Community
db_name = community_db
db_user = odoo
admin_passwd = COMMUNITY_PASSWORD
http_port = 8089
gevent_port = 8092
http_interface = 127.0.0.1
proxy_mode = True
list_db = False
workers = 2
max_cron_threads = 1
limit_memory_hard = 2147483648
logfile = /var/log/odoo/odoo-community.log
log_level = info
EOF
sudo chown odoo:odoo /etc/odoo/odoo-community.conf && sudo chmod 640 /etc/odoo/odoo-community.conf
Odoo 18 Instance (Different Version)

Run an older Odoo version alongside v19 — useful for clients still on v18, migration testing, or module compatibility checks. Requires its own source code and Python venv since different versions have different dependencies.

# ── Clone Odoo 18 source (separate directory from v19) ──
sudo su - odoo
git clone https://github.com/odoo/odoo.git --depth 1 --branch 18.0 /opt/odoo/odoo18-server
# Enterprise (if licensed for v18):
git clone git@github.com:odoo/enterprise.git --depth 1 --branch 18.0 /opt/odoo/enterprise18
exit

# ── Separate Python venv (v18 has different requirements) ──
sudo -u odoo python3 -m venv /opt/odoo/odoo18-venv
sudo -u odoo /opt/odoo/odoo18-venv/bin/pip install --upgrade pip wheel setuptools
sudo -u odoo /opt/odoo/odoo18-venv/bin/pip install -r /opt/odoo/odoo18-server/requirements.txt
sudo tee /etc/odoo/odoo18.conf > /dev/null << 'EOF'
[options]
;; Odoo 18 — Enterprise (includes enterprise18 modules):
addons_path = /opt/odoo/odoo18-server/addons,/opt/odoo/enterprise18,/opt/odoo/custom-addons
;; Odoo 18 — Community-only:
;addons_path = /opt/odoo/odoo18-server/addons,/opt/odoo/custom-addons
data_dir = /opt/odoo/.local/share/Odoo18
db_name = odoo18_db
db_user = odoo
admin_passwd = ODOO18_PASSWORD
http_port = 8099
longpolling_port = 8098
http_interface = 127.0.0.1
proxy_mode = True
list_db = False
workers = 2
max_cron_threads = 1
limit_memory_hard = 2147483648
logfile = /var/log/odoo/odoo18.log
log_level = info
EOF
sudo chown odoo:odoo /etc/odoo/odoo18.conf && sudo chmod 640 /etc/odoo/odoo18.conf
v18 uses longpolling_port, not gevent_port. This rename happened in v19. Each version uses its own parameter name.
Systemd Services (Staging / Community / v18)

Systemd service file for the staging instance:

sudo tee /etc/systemd/system/odoo-staging.service > /dev/null << 'EOF'
[Unit]
Description=Odoo 19 Staging
Requires=postgresql.service
After=network.target postgresql.service
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin -c /etc/odoo/odoo-staging.conf
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable odoo-staging
sudo systemctl start odoo-staging
sudo systemctl status odoo-staging

Community Service

sudo tee /etc/systemd/system/odoo-community.service > /dev/null << 'EOF'
[Unit]
Description=Odoo 19 Community
Requires=postgresql.service
After=network.target postgresql.service
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin -c /etc/odoo/odoo-community.conf
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable odoo-community
sudo systemctl start odoo-community
sudo systemctl status odoo-community

Odoo 18 Service

sudo tee /etc/systemd/system/odoo18.service > /dev/null << 'EOF'
[Unit]
Description=Odoo 18
Requires=postgresql.service
After=network.target postgresql.service
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/opt/odoo/odoo18-venv/bin/python3 /opt/odoo/odoo18-server/odoo-bin -c /etc/odoo/odoo18.conf
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable odoo18
sudo systemctl start odoo18
sudo systemctl status odoo18
Management Commands & Logs
# View logs for specific instance
sudo journalctl -u odoo-staging -f
sudo journalctl -u odoo-community -f
sudo journalctl -u odoo18 -f

# Restart specific instance
sudo systemctl restart odoo-staging

# Stop all Odoo instances
sudo systemctl stop odoo odoo-staging odoo-community odoo18

# Check which ports are in use
sudo netstat -tulpn | grep odoo
# or
sudo ss -tulpn | grep odoo
Nginx Configurations (Staging / Community / v18)

Staging Nginx

sudo tee /etc/nginx/sites-available/odoo-staging > /dev/null << 'EOF'
upstream odoo-stg { server 127.0.0.1:8079; }
upstream odoo-stg-ws { server 127.0.0.1:8082; }
server {
    listen 80;
    server_name staging.example.com;
    client_max_body_size 200M;
    proxy_read_timeout 720s;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    location /longpolling { proxy_pass http://odoo-stg-ws; }
    location /websocket { proxy_pass http://odoo-stg-ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
    location / { proxy_pass http://odoo-stg; }
}
EOF

sudo ln -sf /etc/nginx/sites-available/odoo-staging /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Add SSL certificate
sudo certbot --nginx -d staging.example.com

Community Nginx

sudo tee /etc/nginx/sites-available/odoo-community > /dev/null << 'EOF'
upstream odoo-ce { server 127.0.0.1:8089; }
upstream odoo-ce-ws { server 127.0.0.1:8092; }
server {
    listen 80;
    server_name community.example.com;
    client_max_body_size 200M;
    proxy_read_timeout 720s;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    location /longpolling { proxy_pass http://odoo-ce-ws; }
    location /websocket { proxy_pass http://odoo-ce-ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
    location / { proxy_pass http://odoo-ce; }
}
EOF

sudo ln -sf /etc/nginx/sites-available/odoo-community /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Add SSL certificate
sudo certbot --nginx -d community.example.com

Odoo 18 Nginx

sudo tee /etc/nginx/sites-available/odoo18 > /dev/null << 'EOF'
upstream odoo18 { server 127.0.0.1:8099; }
upstream odoo18-lp { server 127.0.0.1:8098; }
server {
    listen 80;
    server_name v18.example.com;
    client_max_body_size 200M;
    proxy_read_timeout 720s;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    location /longpolling { proxy_pass http://odoo18-lp; }
    location /websocket { proxy_pass http://odoo18-lp; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
    location / { proxy_pass http://odoo18; }
}
EOF

sudo ln -sf /etc/nginx/sites-available/odoo18 /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Add SSL certificate
sudo certbot --nginx -d v18.example.com
Initialize Databases & Verify

Initialize Community Database

# Create and initialize Community database
sudo -u postgres createdb -O odoo community_db
sudo systemctl stop odoo-community
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo-community.conf -d community_db \
    --no-http \
    -i base,web,mail,contacts --without-demo=all --stop-after-init
sudo systemctl start odoo-community

Initialize Odoo 18 Database

# Create and initialize Odoo 18 database (uses its own venv + source)
sudo -u postgres createdb -O odoo odoo18_db
sudo systemctl stop odoo18
sudo -u odoo /opt/odoo/odoo18-venv/bin/python3 /opt/odoo/odoo18-server/odoo-bin \
    -c /etc/odoo/odoo18.conf -d odoo18_db \
    --no-http \
    -i base,web,mail,contacts --without-demo=all --stop-after-init
sudo systemctl start odoo18

Verify All Instances

for svc in odoo odoo-staging odoo-community odoo18; do
    echo -n "$svc: "; systemctl is-active $svc
done

Port Allocation

InstanceEditionVersionHTTPGevent/LPWorkersDomain
ProductionEnterprise19.08069807217erp.example.com
StagingEnterprise19.0807980822staging.example.com
CommunityCommunity19.0808980922community.example.com
Odoo 18Enterprise/CE18.0809980982v18.example.com
v19 instances (production, staging, community) share the same codebase and Python venv — only config, data directories, and ports differ. The Odoo 18 instance has its own source and venv since different versions have different dependencies. Each is an independent service.
⚙️ Part 3: Operations
🔒 Safe Update Procedure

Always: backup → stop → fetch → update schema → clear cache → start. Test on staging first when possible.

Standard Update (GitHub Clone)

# 1. Backup (MANDATORY before any update)
cd /tmp && sudo -u odoo /opt/odoo/scripts/backup.sh

# 2. Stop service
sudo systemctl stop odoo

# 3. Fix Git ownership (first time only, or after manual git operations as root)
sudo chown -R odoo:odoo /opt/odoo/odoo-server
sudo chown -R odoo:odoo /opt/odoo/enterprise

# 4. Fetch and rebase (official Odoo-recommended method)
#    --autostash preserves any local changes you made to source files
cd /opt/odoo/odoo-server && sudo -u odoo git fetch && sudo -u odoo git rebase --autostash
cd /opt/odoo/enterprise && sudo -u odoo git fetch && sudo -u odoo git rebase --autostash  # Skip for Community-only

# 5. Update pip deps (if requirements.txt changed)
sudo -u odoo /opt/odoo/odoo-venv/bin/pip install -r /opt/odoo/odoo-server/requirements.txt

# 6. Update schema (base only — safe for production)
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo.conf -d production_db -u base --stop-after-init
# If specific modules changed: -u base,mrp,sale (check git log for affected modules)

# 7. Clear cached assets
sudo -u postgres psql -d production_db -c "DELETE FROM ir_attachment WHERE name LIKE '%/web/assets/%';"

# 8. Start and verify
sudo systemctl start odoo
sudo systemctl status odoo

# 9. Check recent commits (ALWAYS use 'sudo -u odoo')
cd /opt/odoo/odoo-server && sudo -u odoo git log -5 --oneline origin/19.0
cd /opt/odoo/enterprise && sudo -u odoo git log -5 --oneline origin/19.0

Handling Conflicts

If git rebase --autostash reports source code conflicts (because you edited Odoo source files locally), you have two options:

# Option A: Resolve conflicts manually
#   Git will list the conflicting files. Edit each file,
#   choose which version to keep, then:
cd /opt/odoo/odoo-server
sudo -u odoo git add .
sudo -u odoo git rebase --continue

# Option B: Discard local changes and use official version
cd /opt/odoo/odoo-server && sudo -u odoo git reset --hard origin/19.0
cd /opt/odoo/enterprise && sudo -u odoo git reset --hard origin/19.0
Avoid editing Odoo source files directly. Use custom modules with _inherit to override behavior. This eliminates merge conflicts entirely during updates.
Git Operations Best Practice: ALWAYS run git commands as odoo user (sudo -u odoo git ...) to avoid ownership conflicts. Running git as root causes "dubious ownership" errors. The chown in step 3 fixes existing ownership issues, but consistent use of sudo -u odoo prevents future problems.
Never use -u all — updates every module sequentially, takes hours, and a failure mid-way leaves the database in an inconsistent state requiring restore from backup.
💾 Automated Backups

A complete Odoo backup has two parts: the PostgreSQL database (all records, settings, users) and the filestore (uploaded files, images, attachments). Both are required for a full restore.

sudo mkdir -p /opt/odoo/backups && sudo chown odoo:odoo /opt/odoo/backups
sudo mkdir -p /opt/odoo/scripts && sudo chown odoo:odoo /opt/odoo/scripts

sudo tee /opt/odoo/scripts/backup.sh > /dev/null << 'EOF'
#!/bin/bash
set -e
DB="${1:-production_db}"
DIR="/opt/odoo/backups"
FS="/opt/odoo/.local/share/Odoo/filestore/$DB"
DATE=$(date +%Y%m%d_%H%M%S)
KEEP=3

echo "[$(date)] Backup starting: $DB"

# Database (custom format = compressed + selective restore)
pg_dump -Fc "$DB" > "$DIR/${DB}_${DATE}.dump"

# Filestore
[ -d "$FS" ] && tar -czf "$DIR/${DB}_fs_${DATE}.tar.gz" -C "$(dirname $FS)" "$(basename $FS)"

# Retention
find "$DIR" -type f -name "${DB}_*" -mtime +$KEEP -delete

# Report
echo "[$(date)] Done: $(du -h "$DIR/${DB}_${DATE}.dump" | cut -f1) DB, $(du -h "$DIR/${DB}_fs_${DATE}.tar.gz" 2>/dev/null | cut -f1 || echo 'N/A') filestore"
EOF

sudo chmod +x /opt/odoo/scripts/backup.sh && sudo chown odoo:odoo /opt/odoo/scripts/backup.sh

# Daily backup at 2 AM
echo -e "0 2 * * * odoo cd /tmp && /opt/odoo/scripts/backup.sh >> /var/log/odoo/backup.log 2>&1
" | sudo tee /etc/cron.d/odoo-backup

# Test now (run from /tmp to avoid permission warnings)
cd /tmp && sudo -u odoo /opt/odoo/scripts/backup.sh

# Verify backup files
ls -lh /opt/odoo/backups/
Important: Always run backup scripts from a directory accessible to the odoo user (like /tmp) to avoid "Permission denied" warnings when the script tries to restore the working directory. The backup completes successfully either way, but running from /tmp eliminates unnecessary warnings.

Offsite Backup

Always keep backups on a separate server. If the production disk fails, local backups are lost too.

# Sync to remote server daily (add to crontab)
rsync -avz --delete /opt/odoo/backups/ backup@remote:/backups/odoo/

# Or use rclone for S3/GCS cloud storage
rclone sync /opt/odoo/backups remote:odoo-backups --transfers 4

Verify Backup Integrity

Backups that can't be restored are useless. Schedule monthly restore tests:

# Quick integrity check (validates dump format without restoring)
sudo cat /opt/odoo/backups/production_db_LATEST.dump | pg_restore -l > /dev/null && echo "Dump OK" || echo "CORRUPT"

# Full restore test to a temporary database
sudo -u postgres createdb -O odoo test_restore
sudo cat /opt/odoo/backups/production_db_LATEST.dump | sudo -u postgres pg_restore -d test_restore
sudo -u postgres psql -d test_restore -c "SELECT count(*) FROM res_users;"  # Sanity check
sudo -u postgres dropdb test_restore
🔄 Database Restore

A full Odoo restore requires two parts: the PostgreSQL database dump and the filestore (uploaded files, images, attachments). Missing either part results in broken images or lost data.

Restore on Same Server

Use this when you need to roll back production to a previous backup, or recover from a failed update:

# 1. Stop Odoo
sudo systemctl stop odoo

# 2. List available backups
ls -lh /opt/odoo/backups/

# 3. Drop and recreate the database
sudo -u postgres dropdb --if-exists production_db
sudo -u postgres createdb -O odoo production_db

# 4. Restore database (pipe method — avoids all permission issues)
sudo cat /opt/odoo/backups/production_db_YYYYMMDD.dump \
    | sudo -u postgres pg_restore -d production_db

# For faster restore on multi-core systems (4 parallel jobs):
sudo cat /opt/odoo/backups/production_db_YYYYMMDD.dump \
    | sudo -u postgres pg_restore -j 4 -d production_db

# 5. Restore filestore
sudo -u odoo rm -rf /opt/odoo/.local/share/Odoo/filestore/production_db
sudo -u odoo tar -xzf /opt/odoo/backups/production_db_fs_YYYYMMDD.tar.gz \
    -C /opt/odoo/.local/share/Odoo/filestore/

# 6. Clear cached assets (forces regeneration)
sudo -u postgres psql -d production_db \
    -c "DELETE FROM ir_attachment WHERE name LIKE '%/web/assets/%';"

# 7. Start and verify
sudo systemctl start odoo
sudo systemctl status odoo
Why pipe method? Using sudo cat backup.dump | sudo -u postgres pg_restore bypasses all file permission issues. Root reads the file and pipes it to postgres — no "Permission denied" errors.
pgvector required: Odoo 19 databases contain AI embedding data that requires the pgvector extension. If you see extension "vector" is not available during restore, install it first: sudo apt install postgresql-16-pgvector — this is a PostgreSQL system extension, not a Python package.

Restore on Different Server

Use this when migrating to a new server, or setting up a disaster recovery copy. The target server must have Odoo and PostgreSQL already installed.

# ── On the SOURCE server: Transfer backup files ──

# Option A: Direct SCP transfer
scp /opt/odoo/backups/production_db_YYYYMMDD.dump user@NEW_SERVER:/tmp/
scp /opt/odoo/backups/production_db_fs_YYYYMMDD.tar.gz user@NEW_SERVER:/tmp/

# Option B: rsync (better for large files, supports resume)
rsync -avzP /opt/odoo/backups/production_db_YYYYMMDD.dump user@NEW_SERVER:/tmp/
rsync -avzP /opt/odoo/backups/production_db_fs_YYYYMMDD.tar.gz user@NEW_SERVER:/tmp/
# ── On the TARGET server: Restore ──

# 1. Stop Odoo on target
sudo systemctl stop odoo

# 2. Create database (ensure odoo user exists in PostgreSQL)
sudo -u postgres createdb -O odoo production_db

# 3. Restore database
sudo cat /tmp/production_db_YYYYMMDD.dump \
    | sudo -u postgres pg_restore -d production_db

# 4. Restore filestore
sudo -u odoo mkdir -p /opt/odoo/.local/share/Odoo/filestore/
sudo -u odoo tar -xzf /tmp/production_db_fs_YYYYMMDD.tar.gz \
    -C /opt/odoo/.local/share/Odoo/filestore/

# 5. Verify filestore ownership
sudo chown -R odoo:odoo /opt/odoo/.local/share/Odoo/filestore/production_db

# 6. Update config to match new server (if domain/ports changed)
sudo nano /etc/odoo/odoo.conf
# Verify: db_name, addons_path, data_dir, http_port

# 7. Clear assets and regenerate
sudo -u postgres psql -d production_db \
    -c "DELETE FROM ir_attachment WHERE name LIKE '%/web/assets/%';"

# 8. Start Odoo
sudo systemctl start odoo
Domain change? If the new server uses a different domain, update the web.base.url system parameter: Settings → Technical → Parameters → System Parameters → search for web.base.url and update it to the new domain.

Restore with Database Manager (Web UI)

If you prefer the web interface, enable the Database Manager first (see Database Manager section), then:

# 1. Enable Database Manager
sudo nano /etc/odoo/odoo.conf
# Set: list_db = True, db_name = False, remove db_host
sudo systemctl restart odoo

# 2. Access: https://your-domain.com/web/database/manager
#    Click "Restore Database" → upload the .zip backup file
#    (Odoo Database Manager uses .zip format containing dump.sql + filestore)

# 3. Re-secure after restore
sudo nano /etc/odoo/odoo.conf
# Set: list_db = False, db_name = production_db
sudo systemctl restart odoo
Backup format matters: The Database Manager uses .zip format (containing SQL dump + filestore). The command-line method in this guide uses .dump (pg_dump custom format) + separate filestore .tar.gz. They are not interchangeable — use the matching restore method for each format.

Post-Restore Checklist

# Verify database is accessible
sudo -u postgres psql -d production_db -c "SELECT count(*) FROM res_users;"

# Verify Odoo can start and serve requests
sudo systemctl status odoo
curl -s -o /dev/null -w "%{http_code}" http://localhost:8069/web/login

# Check logs for errors
sudo journalctl -u odoo -n 50 --no-pager | grep -i error

# If restoring to a NON-production environment, neutralize:
# (see Database Cloning section for full neutralization SQL)
🔄 Database Cloning

Clone a production database for staging, testing, or migration. The clone is a complete copy — same data, same filestore, same settings. Always neutralize the clone to prevent it from sending real emails, running cron jobs, or consuming your Enterprise license.

# ── Step 1: Clone the database (PostgreSQL template method) ──
# IMPORTANT: -T (template) requires NO active connections to the source database.
# You must stop production Odoo temporarily, or use the dump+restore method below.
sudo systemctl stop odoo           # Stop production (required for -T method)
sudo systemctl stop odoo-staging   # Stop target instance
sudo -u postgres dropdb --if-exists staging_db
sudo -u postgres createdb -O odoo -T production_db staging_db
# -T = template (binary copy, faster than dump+restore for same PG version)
sudo systemctl start odoo          # Restart production immediately after clone

# ── Step 2: Clone the filestore ──
sudo -u odoo rm -rf /opt/odoo/.local/share/Odoo-Staging/filestore/staging_db
sudo -u odoo cp -a /opt/odoo/.local/share/Odoo/filestore/production_db \
    /opt/odoo/.local/share/Odoo-Staging/filestore/staging_db

# ── Step 3: Neutralize the clone (CRITICAL) ──
sudo -u postgres psql -d staging_db << 'EOSQL'
-- Disable outgoing mail (prevent sending real emails)
UPDATE ir_mail_server SET active = false;
UPDATE fetchmail_server SET active = false;

-- Disable all cron jobs (prevent duplicate scheduled actions)
UPDATE ir_cron SET active = false;

-- Regenerate database secret (avoid session conflicts with production)
UPDATE ir_config_parameter SET value = encode(gen_random_bytes(32), 'hex')
WHERE key = 'database.secret';

-- Clear the database UUID (forces new registration)
UPDATE ir_config_parameter SET value = 'staging-' || encode(gen_random_bytes(16), 'hex')
WHERE key = 'database.uuid';

-- Mark as non-production (optional — helps identify in UI)
UPDATE ir_config_parameter SET value = 'Staging Clone ' || NOW()::date
WHERE key = 'database.enterprise_code';
EOSQL

# ── Step 4: Start staging instance ──
sudo systemctl start odoo-staging
echo "Staging clone ready at staging.example.com"

Alternative: Clone via Dump + Restore (Zero Downtime)

Use this method to avoid stopping production. Also required when cloning across different PostgreSQL versions or to a remote server:

# Dump from production, restore to staging
sudo -u postgres pg_dump -Fc production_db > /tmp/clone.dump
sudo -u postgres dropdb --if-exists staging_db
sudo -u postgres createdb -O odoo staging_db
sudo cat /tmp/clone.dump | sudo -u postgres pg_restore -d staging_db
rm /tmp/clone.dump

# Then run the same neutralization SQL as above
⚠️ Never skip neutralization! A non-neutralized clone will: send real emails to customers, run cron jobs (duplicate purchase orders, payroll runs), and potentially conflict with your Enterprise license registration.
Schedule regular staging refreshes (e.g., weekly) so testers always work with recent production data. Add the clone + neutralize commands to a script in /opt/odoo/scripts/clone-to-staging.sh.
🔑 Enterprise License

Odoo Enterprise requires an active subscription linked to your database. The license is tied to the database.uuid and the number of active users.

# ── Register subscription via web UI ──
# 1. Login as admin → Settings → General Settings
# 2. Scroll to "Activate Odoo Enterprise" section
# 3. Enter your subscription code and click "Register"

# ── Check license status from command line ──
sudo -u postgres psql -d production_db -c "
SELECT key, value FROM ir_config_parameter
WHERE key IN ('database.expiration_date', 'database.enterprise_code', 'database.uuid')
ORDER BY key;"

# ── Verify Enterprise modules are active ──
sudo -u postgres psql -d production_db -t -c "
SELECT CASE
    WHEN EXISTS(SELECT 1 FROM ir_module_module WHERE name='web_enterprise' AND state='installed')
    THEN 'Enterprise Edition ✓'
    ELSE 'Community Edition'
END;"

# ── Check active user count (affects license billing) ──
sudo -u postgres psql -d production_db -c "
SELECT count(*) AS active_users FROM res_users
WHERE active = true AND share = false AND login != '__system__';"
License renewal: If the expiration date passes, Odoo shows a banner but continues to work. However, you lose access to Enterprise module updates and support. Monitor with: SELECT value FROM ir_config_parameter WHERE key='database.expiration_date';
🔧 Service Management Commands

Essential systemctl and journalctl commands for managing Odoo services. Use these for starting, stopping, monitoring, and troubleshooting your Odoo instances.

Systemctl Commands

Control Odoo services (use odoo, odoo-staging, odoo-community, or odoo18 as the service name):

# Start the service
sudo systemctl start odoo

# Stop the service
sudo systemctl stop odoo

# Restart the service
sudo systemctl restart odoo

# Check service status
sudo systemctl status odoo

# Enable service to start on boot
sudo systemctl enable odoo

# Disable service from starting on boot
sudo systemctl disable odoo

# Reload systemd after editing service file
sudo systemctl daemon-reload

# Start all instances at once
sudo systemctl start odoo odoo-staging odoo-community odoo18

# Stop all instances at once
sudo systemctl stop odoo odoo-staging odoo-community odoo18

# Check status of all instances
sudo systemctl status odoo*

Log Viewing (journalctl)

View logs for troubleshooting and monitoring:

# View real-time logs (follow mode)
sudo journalctl -u odoo -f

# View recent logs (last 100 lines)
sudo journalctl -u odoo -n 100

# View logs from the last hour
sudo journalctl -u odoo --since "1 hour ago"

# View logs from today
sudo journalctl -u odoo --since today

# View logs with priority level (error and above)
sudo journalctl -u odoo -p err

# View logs for specific instance
sudo journalctl -u odoo-staging -f
sudo journalctl -u odoo-community -f
sudo journalctl -u odoo18 -f

# Search logs for specific text
sudo journalctl -u odoo | grep "ERROR"

# Export logs to file
sudo journalctl -u odoo --since today > /tmp/odoo-logs.txt

Process Management

Check running Odoo processes and resource usage:

# Check if Odoo is running
ps aux | grep odoo-bin

# Count worker processes
ps aux | grep odoo-bin | grep -v grep | wc -l

# View memory usage per worker
ps aux --sort=-%mem | grep odoo-bin | head -20

# Check which ports are in use
sudo netstat -tulpn | grep odoo
# or
sudo ss -tulpn | grep odoo

# Kill a specific Odoo process (use PID from ps aux)
sudo kill -9 PID

# Note: 'systemctl reload odoo' does NOT work — Odoo has no ExecReload handler.
# Use 'systemctl restart odoo' for a full restart instead.

Quick Troubleshooting Commands

# Check if service is active
systemctl is-active odoo

# Check if service is enabled
systemctl is-enabled odoo

# View last 50 error logs
sudo journalctl -u odoo -p err -n 50

# Check disk space (low disk causes issues)
df -h

# Check PostgreSQL is running
sudo systemctl status postgresql

# Test database connection
sudo -u postgres psql -d production_db -c "SELECT version();"

# Check Nginx is running
sudo systemctl status nginx

# Test Nginx configuration
sudo nginx -t

# Check if Odoo port is accessible
curl -I http://localhost:8069/web/login
🐍 Odoo Shell Recipes

The Odoo shell gives you a Python REPL with full ORM access — essential for data fixes, bulk operations, and debugging. Start it with:

sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin shell \
    -c /etc/odoo/odoo.conf -d production_db --no-http

Common recipes inside the shell:

# Reset a user's password
user = env['res.users'].search([('login', '=', 'admin')])
user.password = 'new_secure_password'
env.cr.commit()

# Find and fix stuck records
stuck = env['sale.order'].search([('state', '=', 'draft'), ('create_date', '<', '2025-01-01')])
print(f"Found {len(stuck)} old draft orders")

# Bulk update field values
env['hr.employee'].search([('department_id.name', '=', 'Production')]).write({'x_shift': 'morning'})
env.cr.commit()

# Check installed modules
installed = env['ir.module.module'].search([('state', '=', 'installed')])
for m in installed.sorted('name'):
    print(f"{m.name}: {m.installed_version}")

# Find records modified today
from datetime import date
today_partners = env['res.partner'].search([('write_date', '>=', date.today().isoformat())])
print(f"{len(today_partners)} partners modified today")

# Export data to CSV
import csv
orders = env['sale.order'].search([('state', '=', 'sale')], limit=100)
with open('/tmp/orders.csv', 'w') as f:
    w = csv.writer(f)
    w.writerow(['ID', 'Customer', 'Amount', 'Date'])
    for o in orders:
        w.writerow([o.id, o.partner_id.name, o.amount_total, o.date_order])

# Recompute a stored computed field
env['sale.order'].search([])._compute_amount_all()
env.cr.commit()

# Always commit changes! Shell doesn't auto-commit.
env.cr.commit()
Always env.cr.commit() after making changes in the shell. Without it, all modifications are rolled back when you exit.
⌨️ Quick Commands
# ══════ Service ══════
sudo systemctl start|stop|restart|status odoo
sudo systemctl start|stop|restart|status odoo-staging
sudo systemctl start|stop|restart|status odoo-community
sudo systemctl start|stop|restart|status odoo18
sudo journalctl -u odoo -f --no-pager

# ══════ Logs ══════
tail -f /var/log/odoo/odoo.log | grep -iE "(error|warning|critical)"
grep "Login failed" /var/log/odoo/odoo.log | tail -20

# ══════ Database ══════
sudo -u postgres psql -c "\l"
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size('production_db'));"
sudo -u postgres pg_dump -Fc production_db > backup.dump

# ══════ License ══════
sudo -u postgres psql -d production_db -c "SELECT key, value FROM ir_config_parameter WHERE key LIKE 'database.%' ORDER BY key;"

# ══════ Module Update (PRODUCTION-SAFE with v19 protection) ══════
# CRITICAL: NEVER update without backup + staging test!
# For Egyptian payroll, protect salary rules FIRST:
# sudo -u postgres psql -d production_db -c "UPDATE ir_model_data SET noupdate=true WHERE model='hr.salary.rule' AND module='l10n_eg_hr_payroll';"

# Step 1: Backup (MANDATORY)
BACKUP_DIR="/opt/odoo/backups"
DATE=$(date +"%Y%m%d_%H%M%S")
sudo -u postgres pg_dump -Fc production_db > $BACKUP_DIR/pre_update_$DATE.dump
tar -czf $BACKUP_DIR/filestore_$DATE.tar.gz /opt/odoo/.local/share/Odoo/filestore/production_db/

# Step 2: Test in staging FIRST
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo-staging.conf -d staging_db -u module_name --stop-after-init

# Step 3: If staging test passed, update production
sudo systemctl stop odoo
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo.conf -d production_db -u module_name --stop-after-init
sudo systemctl start odoo

# Step 4: Verify service started
systemctl is-active odoo && echo "✓ Update successful" || echo "✗ Update failed - check logs"

# ══════ Odoo Shell ══════
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin shell \
    -c /etc/odoo/odoo.conf -d production_db --no-http

# ══════ Cache Clear ══════
sudo -u postgres psql -d production_db -c "DELETE FROM ir_attachment WHERE name LIKE '%/web/assets/%';"

# ══════ Edition Check ══════
sudo -u postgres psql -d production_db -c "SELECT name, state FROM ir_module_module WHERE name='web_enterprise';"
# state='installed' = Enterprise, 'uninstallable' or missing = Community

# ══════ Health Check ══════
curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost:8069/web/login
sudo -u postgres psql -d production_db -c "SELECT count(*) AS active_users FROM res_users WHERE active=true;"
ps aux | grep odoo-bin | grep -v grep | wc -l  # Should be workers+1+cron_threads
df -h /opt/odoo /var/lib/postgresql  # Disk space check

# ══════ Nginx & SSL ══════
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certificates && sudo certbot renew --dry-run

# ══════ Disk Usage ══════
du -sh /opt/odoo/.local/share/Odoo/filestore/*
sudo -u postgres psql -c "SELECT pg_size_pretty(pg_database_size(datname)) as size, datname FROM pg_database ORDER BY pg_database_size(datname) DESC;"

# ══════ Scheduled Tasks ══════
# Health:      Daily 7:00 AM    /etc/cron.d/odoo-health
cat /etc/cron.d/odoo-*
🛠 Part 4: Troubleshooting & Diagnostics
🔍 Common Issues

🌐 Service & Connectivity

"fatal: detected dubious ownership" on git commands
Running git as root in odoo-owned repo. ALWAYS use: sudo -u odoo git pull, sudo -u odoo git log, etc. Fix ownership once with: sudo chown -R odoo:odoo /opt/odoo/odoo-server /opt/odoo/enterprise
502 Bad Gateway
Odoo crashed or not started. Check systemctl status odoo and tail -100 /var/log/odoo/odoo.log
Notifications/chat not working
WebSocket/longpolling broken. In Odoo 19, real-time features use /websocket (primary) and /longpolling (fallback), both on port 8072. Verify Nginx proxies both: ss -tlnp | grep 8072. Check Nginx config has proxy_http_version 1.1 and Upgrade headers for the /websocket location.
CSS/JS broken (white page)
Asset cache corrupted. Fix: DELETE FROM ir_attachment WHERE name LIKE '%/web/assets/%'; then Ctrl+Shift+R in browser.
Cannot login — "Session Expired" loop
Session cookie conflict. Clear browser cookies for the domain, or: DELETE FROM ir_sessions; + restart Odoo.
Workers dying constantly (OOM)
Memory exceeded. Reduce workers or increase limit_memory_hard. Diagnose: dmesg | grep -i "killed process"
Cron jobs not running
Check max_cron_threads > 0 in odoo.conf. Also check Settings → Technical → Scheduled Actions for stuck jobs.
Emails stuck in outbox
SMTP misconfigured or server unreachable. Check: Settings → Technical → Outgoing Mail Servers → Test Connection. Force retry: UPDATE mail_mail SET state='outgoing' WHERE state='exception';

📦 Data & Modules

Internal Server Error after cloning
database.secret was deleted. Regenerate: UPDATE ir_config_parameter SET value=encode(gen_random_bytes(32),'hex') WHERE key='database.secret';
Custom rules reset after update
noupdate flag missing. Fix: UPDATE ir_model_data SET noupdate=true WHERE model='hr.salary.rule';
Broken Studio views
Never delete views — fix XML directly or archive them. Check: SELECT id, name FROM ir_ui_view WHERE arch_db::text LIKE '%error%';
Module won't install — "depends on X"
Missing dependency. Install the required module first: -i dependency_module. Check with: SELECT name, state FROM ir_module_module WHERE name='missing_module';
Duplicate partner records
Common after imports. Find them: SELECT email, count(*) FROM res_partner WHERE email IS NOT NULL GROUP BY email HAVING count(*)>1; Merge via Contacts → Actions → Merge.
"Maximum recursion depth" error
Circular dependency in computed fields. Check @api.depends chains — field A depends on B which depends on A.
Missing attachments after restore
Filestore folder name must match database name. Check: ls /opt/odoo/.local/share/Odoo/filestore/
Restore fails: "extension vector is not available"
Odoo 19 AI features need pgvector. Install it (system-level, not pip): sudo apt install postgresql-16-pgvector then retry restore.

⚡ Performance & Configuration

Slow performance / queries
Check jit = off in PostgreSQL. Run VACUUM ANALYZE;. Enable slow query log: log_min_duration_statement = 1000
PDF missing headers/footers
Unpatched wkhtmltopdf. Install from GitHub, not apt. Verify: wkhtmltopdf --version
License banner showing
Check: SELECT value FROM ir_config_parameter WHERE key='database.expiration_date'; Update to future date.
⚡ Odoo 19 Specific Issues
These issues are unique to Odoo 19 and will not occur in v18 or earlier. If upgrading from v18, check for these problems immediately.
"res.groups has no attribute 'category_id'" after upgrade
v19 restructured res.groups to use privilege_id. Fix custom code: change group.category_id to group.privilege_id.category_id. SQL check: SELECT COUNT(*) FROM res_groups WHERE privilege_id IS NULL AND category_id IS NOT NULL;
JSON API returns 404 after v19 upgrade
Controller type changed from 'json' to 'jsonrpc'. Fix: Update all custom controllers: @http.route('/api/endpoint', type='jsonrpc', auth='user'). Find affected files: grep -rn "type='json'" /opt/odoo/custom-addons/
UoM conversions broken — "uom_category not found"
v19 removed the uom.category table. Fix custom code to use relative_uom_id instead of category_id. Check if table still exists: SELECT COUNT(*) FROM information_schema.tables WHERE table_name='uom_category';
Egyptian payroll salary rules reset after upgrade
Module upgrade overwrote custom computations. Prevention: UPDATE ir_model_data SET noupdate=true WHERE model='hr.salary.rule' AND module='l10n_eg_hr_payroll'; Run BEFORE upgrading module. Check protection: SELECT COUNT(*) FROM ir_model_data WHERE model='hr.salary.rule' AND module='l10n_eg_hr_payroll' AND noupdate=false;
Custom display_name not showing — "name_get() removed"
v19 deprecated name_get(). Override _compute_display_name instead. Example fix:
@api.depends('code', 'name')
def _compute_display_name(self):
    for rec in self: rec.display_name = f"[{rec.code}] {rec.name}"
Note: Do NOT redefine the display_name field itself or add store=True — it's a framework field. Only override the compute method.
"JSONB operations not supported" errors
Using PostgreSQL 12 (v19 requires 13+). Verify version: psql --version. If < 13, upgrade PostgreSQL: sudo apt install postgresql-15 and migrate database.
Service won't start after v19 upgrade
Check logs: sudo journalctl -u odoo -n 100 | grep -i error. Common causes: (1) Missing Python deps: pip install -r /opt/odoo/odoo-server/requirements.txt. (2) Database schema outdated: -u base --stop-after-init. (3) File permissions: sudo chown -R odoo:odoo /opt/odoo/.local/share/Odoo/
Passkey/WebAuthn authentication not available
v19 feature requires HTTPS (SSL) and must be enabled through the UI. Navigate to: Settings → Authentication → Passkeys. Ensure your Nginx is configured with a valid SSL certificate and proxy_mode = True is set in odoo.conf.
"read_group() deprecated" warnings in logs
v19 changed internal API. Replace read_group() with _read_group() or formatted_read_group() in custom modules. Find usage: grep -rn "\.read_group(" /opt/odoo/custom-addons/
"_apply_ir_rules" AttributeError after v19 upgrade
v19 removed _apply_ir_rules(). Replace with: query = self._search(domain, bypass_access=True). Find usage: grep -rn "_apply_ir_rules" /opt/odoo/custom-addons/
"self._context" / "self._uid" AttributeError
v19 deprecated direct context/uid access. Replace self._context with self.env.context and self._uid with self.env.uid. Find usage: grep -rn "self\._context\|self\._uid" /opt/odoo/custom-addons/
Import errors for xlsxwriter or registry
v19 changed import paths. Replace from odoo.tools.misc import xlsxwriter with import xlsxwriter. Replace from odoo import registry with from odoo.modules.registry import Registry. Find: grep -rn "from odoo.tools.misc import xlsxwriter\|from odoo import registry" /opt/odoo/custom-addons/
🔍 Database Diagnostic Queries

Run these directly in PostgreSQL to diagnose performance issues and understand your database:

# Connect to database
sudo -u postgres psql -d production_db
-- ── Top 10 largest tables ──
SELECT relname AS table,
       pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
       pg_size_pretty(pg_relation_size(relid)) AS data_size,
       n_live_tup AS rows
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;

-- ── Active connections and what they're doing ──
SELECT pid, usename, state, query_start, 
       NOW() - query_start AS duration,
       LEFT(query, 80) AS query
FROM pg_stat_activity 
WHERE datname = current_database() AND state != 'idle'
ORDER BY query_start;

-- ── Kill a long-running query (use pid from above) ──
-- SELECT pg_terminate_backend(PID_HERE);

-- ── Tables needing VACUUM (dead tuples > 10K) ──
SELECT relname, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;

-- ── Index usage (unused indexes waste disk + slow writes) ──
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC LIMIT 15;

-- ── User and login activity ──
SELECT u.login, p.name
FROM res_users u
JOIN res_partner p ON u.partner_id = p.id
WHERE u.active = true
ORDER BY u.id LIMIT 20;

-- ── Installed module count and versions ──
SELECT state, count(*) FROM ir_module_module GROUP BY state ORDER BY count DESC;

-- ── Check for duplicate partners (common data issue) ──
SELECT email, count(*) as cnt, string_agg(name, ', ') as names
FROM res_partner WHERE email IS NOT NULL AND active = true
GROUP BY email HAVING count(*) > 1 ORDER BY cnt DESC LIMIT 10;

-- ── Pending/failed cron jobs ──
SELECT cron.cron_name, cron.interval_type, cron.interval_number,
       cron.lastcall, cron.nextcall, cron.active
FROM ir_cron cron WHERE active = true
ORDER BY nextcall;
🚨 Emergency Recovery Procedures

When things go seriously wrong — database corruption, failed upgrade, or accidental data loss:

# ══ SCENARIO 1: Failed module upgrade (database inconsistent) ══
# Restore from the backup you made BEFORE the upgrade
ls -lh /opt/odoo/backups/
sudo systemctl stop odoo
sudo -u postgres dropdb production_db
sudo -u postgres createdb -O odoo production_db
sudo -u postgres pg_restore -d production_db /opt/odoo/backups/LATEST.dump
sudo systemctl start odoo

# ══ SCENARIO 2: Odoo won't start at all ══
# Check the actual error (--no-http avoids port conflict if another instance is running)
sudo systemctl stop odoo
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin \
    -c /etc/odoo/odoo.conf --no-http --log-level=debug 2>&1 | head -100
# Common causes: Python import error, missing dependency, corrupt config
# Or check logs without stopping: sudo journalctl -u odoo -n 50 --no-pager

# ══ SCENARIO 3: Locked out — can't login as admin ══
# Direct SQL won't work — Odoo stores hashed passwords, not plain text.
# Step 1: Open Odoo shell
sudo systemctl stop odoo
sudo -u odoo /opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo-server/odoo-bin shell \
    -c /etc/odoo/odoo.conf -d production_db --no-http
# Step 2: In the Odoo shell — V19: ID 1 = __system__ (internal), ID 2 = admin
env['res.users'].browse(2).password = 'admin'
env.cr.commit()
exit()
# Step 3: Start Odoo and login with admin/admin, then change password in UI immediately
sudo systemctl start odoo

# ══ SCENARIO 4: Database won't start (PostgreSQL error) ══
sudo -u postgres pg_isready                              # Check if PG is running
sudo systemctl status postgresql@16-main --no-pager      # Actual PG service (not wrapper)
sudo tail -30 /var/log/postgresql/postgresql-16-main.log  # PG error log
# If "Cannot allocate memory": check postgresql.auto.conf shared_buffers vs actual RAM
cat /var/lib/postgresql/16/main/postgresql.auto.conf | grep shared_buffers
free -h
# If disk full: clear old logs, backups, or WAL files
du -sh /var/lib/postgresql/16/main/pg_wal/  # WAL can grow huge

# ══ SCENARIO 5: Need to rollback a specific table ══
# If you have a recent backup, restore just one table:
sudo cat /opt/odoo/backups/LATEST.dump | sudo -u postgres pg_restore -d production_db --data-only -t table_name

# ══ SCENARIO 6: Accidental mass delete/update ══
# If you caught it quickly (within seconds), PostgreSQL may still have the data:
# IMMEDIATELY stop Odoo to prevent VACUUM from cleaning up
sudo systemctl stop odoo
# Restore from backup (there's no built-in undo in PostgreSQL)
# See Scenario 1 for full restore, or Scenario 5 for single-table restore
📚 Part 5: Reference
📋 Configuration Reference

Key odoo.conf parameters explained:

ParameterDescriptionProduction Default
addons_pathComma-separated module directoriescommunity,enterprise,custom
workersHTTP worker processes(CPU × 2) + 1
max_cron_threadsBackground scheduled job threads2
limit_memory_hardKill worker if memory exceeds (bytes)2684354560 (2.5 GB)
limit_memory_softRecycle worker after request (bytes)2147483648 (2 GB)
limit_time_cpuMax CPU seconds per request600
limit_time_realMax wall-clock seconds per request1200
limit_requestRecycle worker after N requests8192
db_maxconnMax DB connections per worker pool32
list_dbExpose database managerFalse
proxy_modeTrust X-Forwarded-* from NginxTrue
gevent_portLongpolling/websocket port (Odoo 19)8072
log_levelVerbosity: debug/info/warn/errorwarn
✅ Production Checklist
CategoryItem
SecurityStrong admin_passwd (hashed with pbkdf2)
list_db = False
SSL certificate (auto-renewing)
UFW: ports 8069/8072 blocked
Fail2ban with Odoo filter
PerformanceWorkers = (CPU × 2) + 1
PostgreSQL: jit = off
Nginx static file caching
OperationsDaily automated backups
Restore tested successfully
Offsite backup configured
Log management configured (journalctl retention or logfile)
LicenseEnterprise registered
Expiration date valid
SetupEdition chosen (Enterprise/Community)
Demo data disabled (--without-demo=all)
Default admin password changed
📁 Directory Structure
/opt/odoo/
├── odoo-server/              # v19 Community source
├── enterprise/               # v19 Enterprise modules
├── odoo18-server/            # v18 Community source
├── enterprise18/             # v18 Enterprise modules (if licensed)
├── custom-addons/            # Your custom modules
├── odoo-venv/                # Python venv (v19)
├── odoo18-venv/              # Python venv (v18)
├── backups/                  # Database + filestore backups
└── .local/share/
    ├── Odoo/filestore/       # Production filestore
    ├── Odoo-Staging/filestore/  # Staging filestore
    ├── Odoo-Community/filestore/ # Community filestore
    └── Odoo18/filestore/     # Odoo 18 filestore

/etc/odoo/                    # Configuration files
├── odoo.conf                 #   Production config (v19)
├── odoo-staging.conf         #   Staging config (v19)
├── odoo-community.conf       #   Community config (v19)
└── odoo18.conf               #   Odoo 18 config
/var/log/odoo/                # Log files
/etc/systemd/system/          # Service definitions
├── odoo.service              #   Production service (v19)
├── odoo-staging.service      #   Staging service (v19)
├── odoo-community.service    #   Community (v19)
└── odoo18.service            #   Odoo 18 (v18)
/var/run/odoo.pid             # Process ID file
🪟 Windows: Odoo Command-Line Reference

When Odoo is installed on Windows via the official .exe installer, the directory structure, user names, and commands differ from Linux. This section covers common operations for Windows installations.

Default Paths (Installer)

Odoo Install:     C:\Program Files\Odoo 19.0e.YYYYMMDD\
PostgreSQL:       C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\
Config File:      C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf
pg_hba.conf:      C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\data\pg_hba.conf
Filestore:        C:\Users\Admin\AppData\Local\OpenERP S.A\Odoo\filestore\
Service Name:     odoo-server-19.0
Path varies by version. Replace 19.0e.YYYYMMDD with your actual folder name — check dir "C:\Program Files\" | findstr Odoo to see the exact name.

PowerShell Basics (Run as Administrator)

All commands below require PowerShell as Administrator: right-click Start → Terminal (Admin). Without admin, you'll get System error 5: Access is denied.

# ── Service Management ──
net stop "odoo-server-19.0"
net start "odoo-server-19.0"

# ── View Configuration ──
type "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf"

# ── Edit Configuration ──
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf"

PostgreSQL Commands

The Windows installer creates a PostgreSQL user called openpg (not odoo or postgres like on Linux). All psql and pg_* commands use -U openpg.

# ══════════════════════════════════════════════════════════════
# STEP 1: Run these TWO lines FIRST every time you open PowerShell
# (they reset when you close the window)
# ══════════════════════════════════════════════════════════════
$pg = "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\bin"
$env:PGPASSWORD="your_db_password"
# Find your password: type "C:\...\server\odoo.conf" | findstr db_password

# ══════════════════════════════════════════════════════════════
# STEP 2: Now use any command below
# ══════════════════════════════════════════════════════════════

# ── List all databases ──
& "$pg\psql.exe" -U openpg -d postgres -l

# ── Connect to a database (interactive SQL) ──
& "$pg\psql.exe" -U openpg -d enterprise

# ── Run a quick SQL query ──
& "$pg\psql.exe" -U openpg -d enterprise -c "SELECT count(*) FROM res_users;"

# ── List database roles/users ──
& "$pg\psql.exe" -U openpg -d postgres -c "\du"
Always use -d postgres for admin commands (list databases, alter users, etc.). Without -d, psql tries to connect to a database named after the user (openpg), which may not exist and gives: FATAL: database "openpg" does not exist.
$pg and $env:PGPASSWORD reset every time you close PowerShell. You must set them again in each new session. Alternatively, always use the full path: & "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\bin\psql.exe" -U openpg -d postgres -l

Backup & Restore

Ensure $pg and $env:PGPASSWORD are set first (see above).

# ── Create backup folder ──
mkdir C:\backup

# ── Backup (custom format, compressed) ──
& "$pg\pg_dump.exe" -U openpg -Fc enterprise > C:\backup\enterprise.dump

# ── Restore ──
# 1. Stop Odoo first
net stop "odoo-server-19.0"

# 2. Drop old database (if replacing)
& "$pg\dropdb.exe" -U openpg enterprise

# 3. Create fresh database
& "$pg\createdb.exe" -U openpg enterprise

# 4. Restore from dump
& "$pg\pg_restore.exe" -U openpg -d enterprise "C:\backup\enterprise.dump"

# 5. Start Odoo
net start "odoo-server-19.0"

Restore from Linux Server (Cross-Version)

If the dump was created with a newer PostgreSQL (e.g. v16 on Linux) and your Windows has an older version (e.g. v12), pg_restore will fail with unsupported version (1.15) in file header. Use a plain SQL dump instead:

# ── On the Linux server: Create plain SQL dump ──
sudo -u postgres pg_dump --format=plain --no-owner --no-privileges your_database > /tmp/database_plain.sql

# Compress (1.9GB → ~200MB)
gzip /tmp/database_plain.sql

# Transfer to Windows via SCP, WinSCP, FileZilla, etc.
# ── On Windows: Decompress and restore ──
# Decompress the .gz file using 7-Zip or WinRAR first

# Drop and recreate
& "$pg\dropdb.exe" -U openpg your_database
& "$pg\createdb.exe" -U openpg your_database

# Restore using psql (not pg_restore — plain SQL uses psql)
& "$pg\psql.exe" -U openpg -d your_database -f "C:\path\to\database_plain.sql"

# Verify
& "$pg\psql.exe" -U openpg -d your_database -c "SELECT count(*) FROM res_users;"

# Update odoo.conf with the database name
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf"
# Set: db_name = your_database

# Start Odoo
net start "odoo-server-19.0"
Plain SQL dumps are large (often 1-2GB+ uncompressed). Always gzip on the server before transferring. The restore will take several minutes for large databases.
\unrestrict error at the end? This is a PostgreSQL 16+ command not recognized by older versions. It's harmless — the data is fully restored. Verify with SELECT count(*) FROM res_users;

Password Reset

If you forgot the PostgreSQL password or get password authentication failed:

# 1. Stop Odoo
net stop "odoo-server-19.0"

# 2. Edit pg_hba.conf — change all "md5" to "trust" temporarily
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\data\pg_hba.conf"
# Change lines to:
#   host    all    all    127.0.0.1/32    trust
#   host    all    all    ::1/128         trust

# 3. Start Odoo (restarts PostgreSQL too)
net start "odoo-server-19.0"

# 4. Connect without password and reset (use FULL PATH + -d postgres)
& "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\bin\psql.exe" -U openpg -d postgres -c "ALTER USER openpg WITH PASSWORD 'new_password';"

# 5. Revert pg_hba.conf back to "md5" (CRITICAL for security!)
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\data\pg_hba.conf"
# Change back to:
#   host    all    all    127.0.0.1/32    md5
#   host    all    all    ::1/128         md5

# 6. Update odoo.conf with the new password
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf"
# Set: db_password = new_password

# 7. Restart
net stop "odoo-server-19.0"
net start "odoo-server-19.0"

# 8. Verify (set password variable first)
$env:PGPASSWORD="new_password"
& "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\bin\psql.exe" -U openpg -d postgres -l
🔒 Never leave trust in pg_hba.conf! It allows anyone to connect without a password. Always revert to md5 after resetting.
pg_hba.conf syntax errors cause FATAL: could not load pg_hba.conf. If this happens, re-open the file and check for typos, extra spaces, or missing columns. Each line must have exactly: TYPE DATABASE USER ADDRESS METHOD.

Key Differences: Windows vs Linux

ItemLinux (Ubuntu)Windows (Installer)
PostgreSQL userodooopenpg
Default databaseproduction_db (you name it)enterprise (auto-created)
Service controlsudo systemctl start/stop odoonet start/stop "odoo-server-19.0"
Admin requiredsudoPowerShell as Administrator
Config file/etc/odoo/odoo.confC:\...\server\odoo.conf
Filestore/opt/odoo/.local/share/Odoo/filestore/C:\Users\...\AppData\Local\OpenERP S.A\Odoo\filestore\
Run as service usersudo -u odoo ...Not needed (runs as Windows service)
pg_hba.conf authpeer (Unix socket)md5 (TCP only, password always required)
psql default DBConnects to user's DBMust specify -d postgres
Template databasestemplate0, template1, postgresnever delete these

Common Windows Errors

System error 5: Access is denied
Not running as Administrator. Right-click Start → Terminal (Admin).
The term '\psql.exe' is not recognized
The $pg variable was not set in this session. PowerShell variables reset when you close the window. Set it again: $pg = "C:\Program Files\Odoo 19.0e.YYYYMMDD\PostgreSQL\bin"
FATAL: database "openpg" does not exist
Missing -d flag. Add -d postgres to your command (or the name of your actual database).
FATAL: password authentication failed
Wrong password. Check db_password in odoo.conf, or reset via the pg_hba.conf trust method above.
FATAL: could not load pg_hba.conf
Syntax error in pg_hba.conf. Open it and check each line has exactly: TYPE, DATABASE, USER, ADDRESS, METHOD — separated by spaces/tabs.
FATAL: role "odoo" does not exist / role "postgres" does not exist
Windows installer uses openpg as the PostgreSQL user, not odoo or postgres. Always use -U openpg.
unsupported version (1.15) in file header
The dump was created with a newer PostgreSQL than yours (e.g. dump from v16, restoring on v12). Use plain SQL dump instead: on the source server run pg_dump --format=plain --no-owner, then restore with psql -f instead of pg_restore. See "Restore from Linux Server" above.
invalid command \unrestrict at end of restore
Harmless. This is a PostgreSQL 16+ command not recognized by older versions. Your data is fully restored — verify with SELECT count(*) FROM res_users;
type "vector" does not exist during module upgrade
pgvector extension not installed. See "Install pgvector on Windows" section below. Download pre-compiled binaries, copy to PostgreSQL directory, then run CREATE EXTENSION IF NOT EXISTS vector;
Template databases: template0, template1, and postgres are system databases required by PostgreSQL. Deleting them will prevent creating new databases. Only delete databases you created yourself.

Upgrade PostgreSQL (e.g. 12 → 16)

The Odoo Windows installer bundles an older PostgreSQL (often v12). Odoo 19 requires 13+ minimum (16 recommended for pgvector/AI features). Two methods available:

Method A: Clean Install (Recommended)

Uninstall old PostgreSQL completely, then install new. Simpler and avoids port conflicts.

# ── Step 1: Backup database (BOTH formats for safety) ──
mkdir C:\backup
& "$pg\pg_dump.exe" -U openpg -Fc your_database > C:\backup\database.dump
& "$pg\pg_dump.exe" -U openpg --format=plain --no-owner your_database > C:\backup\database_plain.sql

# ── Step 2: Backup odoo.conf and filestore ──
copy "C:\Program Files\Odoo 19.0\server\odoo.conf" C:\backup\odoo.conf.bak
xcopy "C:\Users\Admin\AppData\Local\OpenERP S.A\Odoo\filestore" "C:\backup\filestore\" /E /I

# ── Step 3: Stop everything ──
net stop "odoo-server-19.0"

# ── Step 4: Uninstall old PostgreSQL ──
# Control Panel → Programs → Uninstall:
#   - Uninstall any standalone PostgreSQL (e.g. PostgreSQL 15)
#   - Uninstall Odoo (removes bundled PostgreSQL 12 with it)
# Or disable old service: sc config "PostgreSQL_For_Odoo" start= disabled

# ── Step 5: Install PostgreSQL 16 ──
# Download from https://www.postgresql.org/download/windows/ (EDB installer)
# During install:
#   Port: 5432 (standard)
#   Superuser (postgres) password: remember it!

# ── Step 6: Install pgvector (see section below) ──

# ── Step 7: Create user and database ──
$pg16 = "C:\Program Files\PostgreSQL\16\bin"
$env:PGPASSWORD="postgres_superuser_password"

& "$pg16\psql.exe" -U postgres -d postgres -c "CREATE USER openpg WITH SUPERUSER CREATEDB PASSWORD 'your_db_password';"

$env:PGPASSWORD="your_db_password"
& "$pg16\createdb.exe" -U openpg your_database

# ── Step 8: Install pgvector extension in database ──
& "$pg16\psql.exe" -U openpg -d your_database -c "CREATE EXTENSION IF NOT EXISTS vector;"

# ── Step 9: Restore ──
# Try custom format first:
& "$pg16\pg_restore.exe" -U openpg -d your_database "C:\backup\database.dump"

# If it fails (version mismatch), use plain SQL:
& "$pg16\psql.exe" -U openpg -d your_database -f "C:\backup\database_plain.sql"

# ── Step 10: Verify ──
& "$pg16\psql.exe" -U openpg -d your_database -c "SELECT count(*) FROM res_users;"
& "$pg16\psql.exe" -U openpg -d your_database -c "SELECT version();"
# ── Step 11: Re-install Odoo ──
# Download from https://www.odoo.com/page/download
# During install, set PostgreSQL connection:
#   Host: localhost, Port: 5432, User: openpg, Password: your_db_password

# ── Step 12: Update odoo.conf ──
notepad "C:\Program Files\Odoo 19.0\server\odoo.conf"
# Set: db_name = your_database

# ── Step 13: Start Odoo ──
net start "odoo-server-19.0"
# Open http://localhost:8069
# Go to Settings → Developer Tools → Update Apps List → Upgrade base module

Method B: Side-by-Side (Keep Old Running)

Install new PostgreSQL alongside old one on a different port, migrate, then switch.

# ── Install PostgreSQL 16 on port 5433 (temporary) ──
# Same EDB installer, but set port to 5433 during install

$pg16 = "C:\Program Files\PostgreSQL\16\bin"
$env:PGPASSWORD="postgres_superuser_password"

# Create user and database on new server
& "$pg16\psql.exe" -U postgres -p 5433 -d postgres -c "CREATE USER openpg WITH SUPERUSER CREATEDB PASSWORD 'your_db_password';"

$env:PGPASSWORD="your_db_password"
& "$pg16\createdb.exe" -U openpg -p 5433 your_database
& "$pg16\psql.exe" -U openpg -p 5433 -d your_database -c "CREATE EXTENSION IF NOT EXISTS vector;"
& "$pg16\pg_restore.exe" -U openpg -p 5433 -d your_database "C:\backup\database.dump"

# After verifying, switch ports:
# Old PostgreSQL → port 5434 (or disable in services.msc)
# New PostgreSQL 16 → port 5432
# Update odoo.conf → db_port = 5432
⚠️ Don't delete old PostgreSQL until you've confirmed everything works on the new one. Keep backups in C:\backup\.
After confirming: Disable old service in services.msc → set Startup Type to "Disabled". Or uninstall from Control Panel.

Install pgvector on Windows (Manual)

Odoo 19 AI features require pgvector. It's not available in StackBuilder, so install it manually using pre-compiled binaries:

# ── Step 1: Download pre-compiled pgvector for your PostgreSQL version ──
# https://github.com/andreiramani/pgvector_pgsql_windows/releases
# Download: "pgvector v0.8.1 for PostgreSQL 16, Microsoft Windows"
# Extract the zip file

# ── Step 2: Copy files to PostgreSQL directory ──
copy "C:\Users\Admin\Downloads\vector.v0.8.1-pg16\lib\*" "C:\Program Files\PostgreSQL\16\lib\"
xcopy "C:\Users\Admin\Downloads\vector.v0.8.1-pg16\share\*" "C:\Program Files\PostgreSQL\16\share\" /E /I /Y

# ── Step 3: Restart PostgreSQL ──
net stop "postgresql-x64-16"
net start "postgresql-x64-16"

# ── Step 4: Enable in your database ──
$pg16 = "C:\Program Files\PostgreSQL\16\bin"
$env:PGPASSWORD="your_db_password"
& "$pg16\psql.exe" -U openpg -d your_database -c "CREATE EXTENSION IF NOT EXISTS vector;"
# Should output: CREATE EXTENSION
Why pgvector? Odoo 19 uses it for AI embeddings (ai_embedding table). Without it, upgrading modules fails with type "vector" does not exist. The extension only needs to be created once per database.
Match versions! Download the pgvector binary that matches your PostgreSQL version exactly. Using pg14 binaries on pg16 will not work.

Odoo Source Update (Windows Installer)

The Windows .exe installer does not use git — it installs Odoo as a standalone application. To update:

Method 1: Re-download Installer (Recommended)

# 1. Backup database
& "$pg\pg_dump.exe" -U openpg -Fc enterprise > C:\backup\enterprise_before_update.dump

# 2. Stop Odoo
net stop "odoo-server-19.0"

# 3. Download latest installer from:
#    https://www.odoo.com/page/download
#    Choose: Odoo 19 Enterprise (or Community) → Windows

# 4. Run the new installer
#    It will detect the existing installation and update in-place
#    Your database and filestore are preserved

# 5. Start Odoo
net start "odoo-server-19.0"

# 6. Update module schema (from browser)
#    Go to: Settings → Developer Tools → Update Apps List
#    Then update specific modules as needed

Method 2: Git Clone (For Developers)

If you want git-based updates like on Linux, install Git for Windows and clone the source:

# 1. Install Git for Windows from https://git-scm.com/download/win

# 2. Clone Odoo source (into the installer's server directory)
cd "C:\Program Files\Odoo 19.0e.YYYYMMDD\server"
git clone https://github.com/odoo/odoo.git --depth 1 --branch 19.0 odoo
git clone git@github.com:odoo/enterprise.git --depth 1 --branch 19.0 enterprise

# 3. Update odoo.conf to point to the git source
notepad "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo.conf"
# Update addons_path to include:
#   addons_path = C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo\addons,C:\Program Files\Odoo 19.0e.YYYYMMDD\server\enterprise

# 4. Update using same method as Linux
cd "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\odoo"
git fetch
git rebase --autostash

cd "C:\Program Files\Odoo 19.0e.YYYYMMDD\server\enterprise"
git fetch
git rebase --autostash

# 5. Restart Odoo
net stop "odoo-server-19.0"
net start "odoo-server-19.0"

# 6. If conflicts occur — discard local changes:
git reset --hard origin/19.0
Installer vs Git: The .exe installer is easier to manage — just re-download and reinstall. Git clone gives you daily updates and more control, but requires manual Python environment setup. For production Windows deployments, the installer method is recommended.