HADOOPT
TECHNOLOGIES
Schedule a Tech Consultation
Home
Services
Odoo ERP implementation Rollout, custom modules, migration and support. Web application development SaaS products, portals and internal platforms. Mobile application development Offline-first field apps and customer apps. AI development & automation LLM assistants, document processing, forecasting. Odoo version upgrades ↗ Move to a newer release, scoped and priced on its own site.
Products
OrgExp ↗ Budget, CAPEX and OPEX management.
Blog About Contact Schedule a Tech Consultation
Blog / Odoo implementation
Implementation 7 min read

Running Odoo on-premise: backups, monitoring and performance tuning

Self-hosting Odoo 19 properly: pg_dump plus the filestore, off-site copies, restore tests, health checks, workers, memory limits, PostgreSQL and Nginx.

· ERP practice 12 Nov 2026

Running Odoo on-premise well comes down to three habits. Back up both the PostgreSQL database and the filestore, keep copies off the server, and restore one regularly to prove it works. Watch the server's health, disk and logs. And run Odoo in multi-process mode with workers and memory limits sized to your users, behind a reverse proxy that handles HTTPS and websockets.

This assumes you have already chosen to self-host; our hosting comparison covers that decision, and installing Odoo 19 covers the install. The settings below are from Odoo 19's deployment documentation and source unless stated.

What an Odoo backup must contain

An Odoo database is two things, and a backup with only one of them is not a backup.

  • The PostgreSQL database: every record, setting and customisation, including Studio changes.
  • The filestore: attachments, product images, PDFs and other binary files, kept on disk at <data_dir>/filestore/<database name>. Restore the database without it and every attachment link breaks.

Also keep your odoo.conf and your custom and third-party modules, ideally in Git, so you can rebuild the server and not only the data. PostgreSQL roles are not in a database dump; the PostgreSQL docs point to pg_dumpall --globals-only for those.

Odoo's own backup, from the database manager, is a zip of a pg_dump SQL file, the filestore and a manifest.json. It is convenient, but Odoo's deployment guide strongly recommends disabling the database manager on internet-facing servers (list_db = False) and having the system administrator run backups instead. So script them.

A backup script with pg_dump and the filestore

pg_dump is safe to run during working hours. The PostgreSQL documentation says it makes consistent exports even while the database is in use, and does not block readers or writers. The custom format (-Fc) is compressed and restored with pg_restore.

#!/bin/sh
set -eu
DB=mycompany
DATA_DIR=/var/lib/odoo            # the data_dir in your odoo.conf
DEST=/backup/odoo
STAMP=$(date +%Y-%m-%d_%H%M)

mkdir -p "$DEST"
# 1. The database first, as the odoo database user
pg_dump --format=custom --no-owner --dbname="$DB" --file="$DEST/$DB-$STAMP.dump"

# 2. Then the filestore, so every file the dump refers to is included
tar -czf "$DEST/$DB-$STAMP-filestore.tar.gz" -C "$DATA_DIR/filestore" "$DB"

# 3. Off-site copy, then local retention
rsync -a "$DEST/" [email protected]:/srv/odoo-backups/
find "$DEST" -type f -mtime +14 -delete
A nightly backup run from cron. Paths, the host and the 14-day local retention are examples; adjust them to your server.

Dumping first and copying the filestore second means any file added between the two steps is an extra file, not a missing one. Extra files do no harm.

Testing a restore

A backup you have never restored is a guess. Restore to a separate server or a separate database name at least monthly, and after any change to the backup script.

createdb mycompany_restore
pg_restore --no-owner --dbname=mycompany_restore mycompany-2026-11-12_0200.dump
mkdir -p /var/lib/odoo/filestore
tar -xzf mycompany-2026-11-12_0200-filestore.tar.gz -C /var/lib/odoo/filestore
mv /var/lib/odoo/filestore/mycompany /var/lib/odoo/filestore/mycompany_restore

# Stop the copy sending emails or running live connectors
odoo-bin neutralize -c /etc/odoo/odoo.conf -d mycompany_restore
Restoring under a new name, run as the odoo system user. The filestore folder must match the new database name.

The neutralize command is in Odoo's CLI. It prepares a copy for testing so that, for example, it does not send emails. Then log in, open a few documents with attachments, print an invoice and check the latest records are there. Write down how long the whole restore took. That number is your real recovery time.

Monitoring an Odoo server

You don't need a large monitoring stack, but someone has to be told when something breaks.

  • Uptime: Odoo 19 answers /web/health with a small JSON status, without logging in. Adding ?db_server_status=1 also checks it can reach PostgreSQL. Point an external uptime checker at it.
  • Disk: the filestore, logs and local backups all grow. A full disk is one of the commonest ways a self-hosted Odoo stops.
  • Memory and CPU: watch the load average against your worker count, and look for workers being killed and restarted in the Odoo log, which means they hit their limits.
  • Backups: alert when a nightly backup is missing or much smaller than the last one, not only when the job fails.
  • Certificates and updates: HTTPS certificate expiry, operating system patches and Odoo's own source updates, which Odoo's guide says to install regularly.
  • Slow queries: PostgreSQL's log_min_duration_statement logs queries slower than a threshold you set, which is the quickest way to find a report or custom module that is hurting everyone.

Workers and memory limits in odoo.conf

By default Odoo runs threaded, with workers = 0. For production, Odoo's guide recommends the multi-processing server: set workers to a number above zero, and Odoo starts a pool of HTTP workers, cron workers and a separate gevent worker for websockets (live chat and notifications) on port 8072.

Odoo's rules of thumb for sizing are (CPU cores × 2) + 1 as the maximum number of workers, and roughly 6 concurrent users per worker. For memory, it estimates a light worker at around 150 MB and a heavy one at around 1 GB. Its worked example, 4 CPUs and 60 concurrent users, lands on 8 HTTP workers plus 1 cron worker, and about 3 GB of RAM for Odoo:

[options]
workers = 8
max_cron_threads = 1
; 600 MB: the worker is recycled after its current request
limit_memory_soft = 629145600
; 1.6 GB: memory allocations fail above this
limit_memory_hard = 1677721600
limit_time_cpu = 600
limit_time_real = 1200
limit_request = 8192
proxy_mode = True
list_db = False
Based on the sample in Odoo 19's deployment guide, with proxy_mode and list_db added from the same guide.
OptionDefault in Odoo 19What it does
workers0 (threaded)Number of HTTP worker processes
max_cron_threads2Workers running scheduled actions
limit_memory_soft2048 MiBVirtual memory after which a worker is recycled once its request ends
limit_memory_hard2560 MiBVirtual memory at which allocations fail
limit_time_cpu / limit_time_real60 s / 120 sCPU and wall-clock time allowed per request
db_maxconn64Maximum PostgreSQL connections per Odoo process

The memory limits apply per worker and count virtual memory, which runs higher than what top shows as resident. Raise the time limits only for known long jobs such as large imports or reports, not to hide a slow screen. Because db_maxconn is per process, keep PostgreSQL's max_connections comfortably above what all your workers could open together.

PostgreSQL and the reverse proxy

Odoo 19 needs PostgreSQL 13 or later. Run Odoo as a database user that is not a PostgreSQL superuser, as Odoo's guide requires. For a dedicated database server, PostgreSQL's documentation suggests 25% of system memory as a reasonable starting value for shared_buffers. If Odoo and PostgreSQL share one machine, budget the RAM for both before you add workers.

In front of Odoo, put a reverse proxy such as Nginx. Odoo's guide uses it for three jobs: HTTPS termination, since Odoo sends credentials in clear text otherwise; forwarding /websocket requests to the gevent port (8072) with the upgrade headers; and passing everything else to port 8069. Turn on proxy_mode so Odoo trusts the proxy's forwarded host, scheme and IP, and only when it is behind one. The same proxy is the place to limit request sizes, rate-limit login attempts and block /web/database routes.

Self-hosting is cheap to start and expensive to neglect. Most of the neglect is one untested restore.

Before a version upgrade, all of this matters twice: you need a verified backup and a restored copy to test on. Our upgrade guide starts from there, and our security checklist covers the server around Odoo. If you'd like a second pair of eyes on your setup, talk to us.

Questions we get asked

How do I back up a self-hosted Odoo database?

Back up two things: the PostgreSQL database, with pg_dump, and the filestore folder at data_dir/filestore/database-name, which holds attachments and images. Dump the database first, then archive the filestore, and copy both to storage off the server. Keep odoo.conf and your custom modules in Git. Odoo recommends disabling the web database manager on internet-facing servers, so schedule backups from the command line instead.

How many workers should an Odoo server have?

Odoo's deployment guide gives two rules of thumb: at most (CPU cores × 2) + 1 workers, and about six concurrent users per worker. Its example of 4 CPUs and 60 concurrent users uses 8 HTTP workers and 1 cron worker, with about 3 GB of RAM for Odoo. Treat this as a starting point, then watch CPU load and memory and adjust.

What do limit_memory_soft and limit_memory_hard do in Odoo?

They cap the virtual memory each Odoo worker may use. When a worker passes limit_memory_soft, Odoo recycles it after its current request finishes. At limit_memory_hard, memory allocations fail. Odoo 19's defaults are 2048 MiB and 2560 MiB; the sample in its deployment guide uses 600 MB and 1.6 GB. Both apply per worker, so multiply by the worker count when sizing RAM.

Why does Odoo need a reverse proxy like Nginx?

Odoo sends login credentials in clear text, so production needs HTTPS, which Odoo's guide puts in a proxy such as Nginx. In multi-worker mode the proxy also routes /websocket requests to Odoo's gevent port, 8072, for live chat and notifications. Enable proxy_mode in odoo.conf so Odoo reads the real client address and scheme from the proxy's headers.

← Previous What an Odoo implementation costs in India: licences, services and how to budget Next → Odoo's API for integrations: JSON-2, XML-RPC, webhooks and MCP
Keep reading
Implementation Connecting Odoo with Shopify and Amazon: connectors, sync and source of truth 8 min read Implementation Odoo Studio vs custom modules: the costs of each for upgrades, Git and licensing 7 min read Comparisons Odoo vs ERPNext for Indian businesses 9 min read