Odoo Community is a genuinely capable ERP, free under the LGPL, covering accounting, inventory, CRM, manufacturing and more from one codebase. Installing it is straightforward. What separates a responsive deployment from one that stalls whenever two people use it is the worker configuration, a second port most guides omit, and a backup that includes the filestore rather than only the database.
Architecturally it is a Python application sitting in front of PostgreSQL, and that shape explains most of its operational behaviour. Odoo forks a pool of worker processes, each carrying its own interpreter and its own caches, so available memory rather than clock speed sets how many people can use the system comfortably. Almost everything below follows from that one fact.
MassiveGRID Ubuntu VPS: Ubuntu 24.04 LTS · Proxmox HA cluster with automatic failover · Ceph 3x replicated NVMe storage · independent CPU, RAM and storage scaling · 100% uptime SLA · 24/7 human support
Cloud VPS — from $1.99/mo
Dedicated VPS for guaranteed cores · Fully managed hosting
Community or Enterprise
Community is LGPL and free. It covers the core business applications and is what this guide installs. Enterprise adds accounting features that matter in some jurisdictions, full studio customisation, mobile apps, and official support, priced per user per month.
The honest test is your accountant. If local statutory reporting, bank synchronisation or payroll for your country is Enterprise-only, no amount of self-hosting saves you, and finding that out after the data migration is expensive. Confirm the specific compliance requirements before committing.
Sizing and the Worker Formula
Odoo forks worker processes, and each one holds its own Python interpreter and cached data. That makes memory, not CPU, the binding constraint.
The conventional formula is workers equal to twice the core count plus one, then roughly 1 GB of RAM budgeted per worker plus overhead for PostgreSQL:
| Concurrent users | Configuration | Workers | MassiveGRID VPS |
|---|---|---|---|
| 1–5, or evaluation | 2 vCPU / 4 GB GB | 2 | $9.58/mo |
| 5–20 | 4 vCPU / 8 GB GB | 5 | $19.16/mo |
| 20–60 | 4 vCPU / 16 GB GB | 9 | $26.84/mo |
| 60–150 | 8 vCPU / 16 GB GB | 17 | $38.32/mo |
Those figures assume ordinary transactional use. Manufacturing with deep bills of materials, or heavy reporting, shifts the requirement upward considerably, and the symptom is a worker hitting its memory limit mid-report rather than the server running out of RAM.
Installing Odoo
Use the official repository. Building from source is only worth it if you are developing modules.
apt update && apt install -y postgresql
sudo -u postgres createuser -d -R -S odoo
wget -qO- https://nightly.odoo.com/odoo.key | gpg --dearmor > /usr/share/keyrings/odoo.gpg
echo "deb [signed-by=/usr/share/keyrings/odoo.gpg] https://nightly.odoo.com/18.0/nightly/deb/ ./" \
> /etc/apt/sources.list.d/odoo.list
apt update && apt install -y odoo
Note the -d on createuser: the Odoo role needs permission to create databases, because Odoo creates and manages its own.
PDF reports need a specific patched build of wkhtmltopdf. The version in the Ubuntu repositories renders headers and footers incorrectly, and the resulting invoices look subtly wrong rather than obviously broken. Install the patched package from the wkhtmltopdf project releases rather than from apt.
The Production Config
Everything that matters lives in /etc/odoo/odoo.conf:
[options]
admin_passwd = a-long-generated-master-password
db_host = False
db_port = False
db_user = odoo
db_password = False
addons_path = /usr/lib/python3/dist-packages/odoo/addons
; production hardening
list_db = False
dbfilter = ^odoo$
proxy_mode = True
; concurrency
workers = 5
max_cron_threads = 2
; memory limits per worker
limit_memory_soft = 1073741824
limit_memory_hard = 1342177280
limit_time_cpu = 600
limit_time_real = 1200
limit_request = 8192
Four of those are the ones people regret omitting.
list_db = False hides the database manager. Left enabled, anyone reaching the server sees a page offering to create, duplicate, back up or drop databases, gated only by the master password. It is the most common serious misconfiguration in public Odoo deployments.
workers above zero switches Odoo from threaded to multiprocess mode. This is what makes it usable for more than one person, and it has a consequence covered in the next section.
proxy_mode = True tells Odoo to trust the forwarded headers from nginx. Without it, generated URLs use the internal address and redirects break.
The memory limits cause a worker exceeding the soft limit to be recycled after finishing its request, and one exceeding the hard limit to be killed immediately. Without them, a runaway report consumes the machine.
The Second Port Nobody Expects
With workers enabled, Odoo serves long-polling connections, used for chat, activity notifications and the bus, on a second port. It is 8072 by default, separate from 8069 for normal requests.
Miss it in the proxy configuration and Odoo appears to work while notifications never arrive and the discussion module sits silent. It is the equivalent of the WebSocket problem in other applications, and it presents the same way: no errors, just nothing happening.
upstream odoo {
server 127.0.0.1:8069;
}
upstream odoolongpoll {
server 127.0.0.1:8072;
}
server {
listen 443 ssl;
http2 on;
server_name erp.example.com;
ssl_certificate /etc/letsencrypt/live/erp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.example.com/privkey.pem;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 100M;
proxy_read_timeout 720s;
proxy_send_timeout 720s;
location /websocket {
proxy_pass http://odoolongpoll;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /longpolling {
proxy_pass http://odoolongpoll;
}
location / {
proxy_pass http://odoo;
proxy_redirect off;
}
gzip on;
gzip_types text/css text/plain application/javascript application/json;
}
Recent Odoo versions use /websocket; older ones use /longpolling. Configuring both is harmless and saves a debugging session after an upgrade.
Tuning PostgreSQL
Odoo is database-heavy and the PostgreSQL defaults are conservative. On an 8 GB server, reasonable starting values:
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 32MB
maintenance_work_mem = 512MB
random_page_cost = 1.1
effective_io_concurrency = 200
The last two matter on NVMe. The defaults assume spinning disks and make the planner reluctant to use index scans, which on Odoo's schema is exactly the wrong instinct. Our PostgreSQL guide covers the rest.
Backups That Include the Filestore
Odoo splits its data. Records live in PostgreSQL; attachments, product images and generated documents live on disk under ~odoo/.local/share/Odoo/filestore. A database-only backup restores an ERP whose every attachment is a broken link.
#!/bin/bash
set -euo pipefail
D=$(date +%F)
sudo -u odoo pg_dump odoo | gzip > /backup/odoo-db-$D.sql.gz
tar czf /backup/odoo-filestore-$D.tar.gz \
/var/lib/odoo/.local/share/Odoo/filestore
cp /etc/odoo/odoo.conf /backup/odoo-conf-$D
find /backup -name 'odoo-*' -mtime +30 -delete
Restore into a test database and open an invoice with an attachment. That single check catches the filestore mistake, and nothing else does.
Upgrades Are the Real Commitment
This is the part to understand before choosing self-hosted Odoo. Major version upgrades migrate the database schema, and custom modules frequently need code changes to follow. Odoo publishes roughly annually and supports three versions at a time, so staying current is ongoing work rather than an occasional event.
Never upgrade in place. Restore a copy of production to a separate instance, run the upgrade there, have the people who use the modules test them, and only then schedule the real one. An ERP that is down is a business that cannot invoice, so the maintenance window is a business decision rather than a technical one.
The corollary is to keep customisation minimal. Every custom module is a thing you will port every year. Configuration inside Odoo survives upgrades; code around it does not, without effort.
Infrastructure for a System the Business Runs On
An ERP is the least forgiving workload most organisations self-host. It holds the accounting records, and an hour of downtime stops order processing, invoicing and dispatch simultaneously.
Every MassiveGRID VPS sits on a Proxmox high-availability cluster with automatic failover, so a failed node migrates the workload instead of ending the day. Storage is Ceph with three-way replication across independent NVMe drives, which is the durability an accounting database warrants. Because CPU, RAM and storage scale independently, adding the memory that more Odoo workers need does not mean rebuying the server.
For a system this critical, fully managed hosting is worth pricing against your own time: patching, verified backups and monitoring handled, so an upgrade window is planned rather than discovered. Otherwise start with a Dedicated VPS, since guaranteed cores matter when month-end reporting and daily transactions compete.