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
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
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/healthwith a small JSON status, without logging in. Adding?db_server_status=1also 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_statementlogs 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
| Option | Default in Odoo 19 | What it does |
|---|---|---|
workers | 0 (threaded) | Number of HTTP worker processes |
max_cron_threads | 2 | Workers running scheduled actions |
limit_memory_soft | 2048 MiB | Virtual memory after which a worker is recycled once its request ends |
limit_memory_hard | 2560 MiB | Virtual memory at which allocations fail |
limit_time_cpu / limit_time_real | 60 s / 120 s | CPU and wall-clock time allowed per request |
db_maxconn | 64 | Maximum 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.