Your Odoo Installation Isn't "Done" When the Login Page Loads
Every Odoo deployment starts with an installation. And every production incident we've investigated at Octura starts with an installation that was "good enough for now." The login page appeared, the demo data loaded, and someone called it done. Six months later: the database can't handle 50 concurrent users, backups have never been tested, and the server runs as root with the default master password.
Odoo 19 has raised the stakes. The runtime now requires PostgreSQL 15+, ships with stricter werkzeug session handling, and the asset pipeline expects rtlcss and wkhtmltopdf 0.12.7 at specific paths. Install it wrong and you get cryptic 500 errors on PDF reports, session timeouts under load, and a system that silently degrades when you need it most.
This guide covers three installation methods—Docker, source install, and Odoo.sh—with the production hardening steps that separate a staging toy from a system you can bet your operations on.
Docker vs Source vs Odoo.sh: Which Odoo 19 Installation Method Fits Your Team
There's no single "right" way to install Odoo. The correct method depends on your team's ops maturity, compliance requirements, and how many custom modules you're running. Here's the honest comparison:
| Aspect | Docker | Source Install | Odoo.sh |
|---|---|---|---|
| Setup time | 15 minutes | 45–90 minutes | 5 minutes |
| Custom modules | Mount volume or custom image | Full filesystem access | Git submodules only |
| PostgreSQL control | Separate container, tunable | Full control, any version | Managed (limited tuning) |
| Scaling | Horizontal via orchestration | Manual multi-worker setup | Built-in (with plan limits) |
| SSL/Proxy | Reverse proxy required | Nginx/Caddy required | Included |
| Backups | You manage (pg_dump + filestore) | You manage (pg_dump + filestore) | Automatic daily |
| Cost | Your infrastructure only | Your infrastructure only | From $32/month + Odoo license |
| Best for | Teams with DevOps capacity | Deep customization, regulated industries | Small–mid teams wanting managed ops |
For most mid-market companies, Docker with a managed PostgreSQL service (AWS RDS, Google Cloud SQL, or DigitalOcean Managed DB) hits the sweet spot. You get reproducible deployments without managing database backups, replication, and failover yourself. Source installs are reserved for clients in regulated industries who need kernel-level auditability.
Production-Grade Docker Compose for Odoo 19: The Full Stack
The official Odoo Docker image gets you to a login screen. It does not get you to production. Below is the docker-compose.yml we use as a starting point for client deployments—with PostgreSQL tuning, persistent volumes, proper resource limits, and security hardening.
version: "3.8"
services:
odoo:
image: odoo:19.0
container_name: odoo19
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8069:8069"
- "127.0.0.1:8072:8072" # longpolling / websocket
volumes:
- odoo-data:/var/lib/odoo
- ./custom-addons:/mnt/extra-addons:ro
- ./config/odoo.conf:/etc/odoo/odoo.conf:ro
environment:
- HOST=db
- USER=odoo
- PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
memory: 4G
cpus: "2.0"
db:
image: postgres:16-alpine
container_name: odoo19-db
restart: unless-stopped
environment:
- POSTGRES_USER=odoo
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=postgres
volumes:
- pg-data:/var/lib/postgresql/data
- ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
command: postgres -c config_file=/etc/postgresql/postgresql.conf
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 2G
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
odoo-data:
pg-data:Key details most tutorials skip:
- Ports bound to 127.0.0.1 — never expose Odoo directly to the internet. Nginx or Caddy sits in front.
- Docker secrets for the database password — not environment variables that leak into
docker inspectoutput. - Health check on PostgreSQL — Odoo won't crash-loop waiting for a slow database start.
- Custom addons mounted read-only — prevents accidental writes from the Odoo process.
- Memory limits — without these, a runaway report generation will OOM-kill your entire host.
[options]
; ── Security ──────────────────────────────────
admin_passwd = False
list_db = False
proxy_mode = True
; ── Performance ───────────────────────────────
workers = 4
max_cron_threads = 2
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limit_time_real = 1200
limit_time_real_cron = 1800
; ── Database ──────────────────────────────────
db_host = db
db_port = 5432
db_user = odoo
db_name = production
dbfilter = ^production$
; ── Paths ─────────────────────────────────────
addons_path = /mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons
data_dir = /var/lib/odoo Setting admin_passwd = False disables the database manager entirely. This is non-negotiable for production. The database manager allows anyone to create, duplicate, drop, or restore databases—with only a single password protecting the entire operation. Combined with list_db = False and dbfilter, this locks the instance to a single database with no web-based management surface.
Installing Odoo 19 from Source on Ubuntu 24.04: Step-by-Step with Security Hardening
Source installs give you full control. They're also the easiest to misconfigure. This is the sequence we follow for bare-metal and VM deployments.
System Dependencies
# System user — never run Odoo as root
sudo adduser --system --home=/opt/odoo --group odoo
# PostgreSQL 16
sudo apt install -y postgresql-16 postgresql-client-16
# Create DB role (no superuser, no createdb in production)
sudo -u postgres createuser --createdb --no-superuser \
--no-createrole odoo
# Python build deps + Odoo runtime deps
sudo apt install -y \
python3-dev python3-pip python3-venv \
libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev \
libpq-dev libjpeg-dev zlib1g-dev \
node-less npm git
# wkhtmltopdf 0.12.7 (Odoo 19 requirement)
wget https://github.com/wkhtmltopdf/packaging/releases/\
download/0.12.7-1/wkhtmltox_0.12.7-1.jammy_amd64.deb
sudo dpkg -i wkhtmltox_0.12.7-1.jammy_amd64.deb
sudo apt install -f -y
# rtlcss for RTL language support
sudo npm install -g rtlcssClone and Configure
# Clone Odoo 19 (shallow clone saves ~2GB)
sudo -u odoo git clone --depth 1 --branch 19.0 \
https://github.com/odoo/odoo.git /opt/odoo/odoo
# Virtual environment (isolated from system Python)
sudo -u odoo python3 -m venv /opt/odoo/venv
sudo -u odoo /opt/odoo/venv/bin/pip install --upgrade pip wheel
sudo -u odoo /opt/odoo/venv/bin/pip install \
-r /opt/odoo/odoo/requirements.txt
# Enterprise (if licensed)
sudo -u odoo git clone --depth 1 --branch 19.0 \
https://github.com/odoo/enterprise.git /opt/odoo/enterprise
# Custom addons
sudo mkdir -p /opt/odoo/custom-addons
sudo chown odoo:odoo /opt/odoo/custom-addonsSystemd Service
[Unit]
Description=Odoo 19
After=postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/opt/odoo/venv/bin/python /opt/odoo/odoo/odoo-bin \
-c /etc/odoo/odoo.conf
StandardOutput=journal
StandardError=journal
# Security hardening
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/odoo/.local /var/log/odoo
NoNewPrivileges=true
PrivateTmp=true
# Restart policy
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target The ProtectSystem=strict and NoNewPrivileges=true directives make the entire filesystem read-only for the Odoo process except explicitly allowed paths. This is defense-in-depth: even if an attacker gains code execution through a custom module vulnerability, they can't modify system binaries or escalate privileges.
Nginx Reverse Proxy
upstream odoo_backend {
server 127.0.0.1:8069;
}
upstream odoo_longpoll {
server 127.0.0.1:8072;
}
server {
listen 443 ssl http2;
server_name erp.yourcompany.com;
ssl_certificate /etc/letsencrypt/live/erp.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.yourcompany.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Odoo request size (large imports, attachments)
client_max_body_size 256m;
# Timeouts for long operations (reports, exports)
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
location / {
proxy_pass http://odoo_backend;
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;
proxy_redirect off;
}
location /websocket {
proxy_pass http://odoo_longpoll;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
# Block database manager from outside
location ~* ^/web/database {
deny all;
return 404;
}
# Static file caching
location ~* /web/static/ {
proxy_pass http://odoo_backend;
expires 365d;
add_header Cache-Control "public, immutable";
}
}3 Installation Mistakes That Cripple Odoo 19 in Production
Running with workers = 0 (Single-Threaded Mode)
The default odoo.conf ships with workers = 0, which runs Odoo in single-threaded mode. This means one slow report generation blocks every other user. A PDF export that takes 30 seconds locks the entire application for all 50 users. We've seen companies run like this for months, blaming "Odoo is slow" when the real culprit is a configuration default meant for development.
Set workers = (CPU cores * 2) + 1 as a baseline. For a 4-core server, that's 9 workers. Then set max_cron_threads = 2 separately so scheduled actions don't steal HTTP worker slots. Monitor with htop during peak hours and adjust until CPU stays under 70%.
PostgreSQL Running on Default Settings
A fresh PostgreSQL 16 install allocates 128MB of shared buffers—enough for a personal blog, not an ERP handling 10,000 journal entries. We routinely find Odoo servers where the database is the bottleneck, but nobody checked pg_stat_user_tables because "the database was set up by the hosting provider."
Minimum tuning for an Odoo production database on a 16GB RAM server: shared_buffers = 4GB, effective_cache_size = 12GB, work_mem = 64MB, maintenance_work_mem = 1GB. Use PGTune as a starting point, then profile with pg_stat_statements to find the actual slow queries.
No Automated Backup Strategy
"We'll set up backups after go-live" is the most expensive sentence in ERP deployment. Odoo's filestore (attachments, images, reports) lives on disk outside PostgreSQL. A pg_dump alone restores a database with broken attachment links. We've been called into recovery situations where a company lost 6 months of uploaded purchase orders because nobody backed up /var/lib/odoo/filestore.
We deploy a cron job that runs pg_dump and rsync of the filestore directory to offsite storage (S3, Backblaze B2, or a separate server). Backups are encrypted with gpg, retention is 30 daily + 12 monthly, and we run a monthly restore test on a staging server to prove the backup actually works. An untested backup is not a backup.
What Proper Installation Saves Your Business
Installation isn't a one-time cost—it's the foundation that determines your total cost of ownership. Here's what we see in the field:
Proper worker config and PostgreSQL tuning eliminate the #1 complaint in Odoo deployments.
Automated, tested backups mean you never pay for emergency data recovery—which averages $5,000–$25,000.
With Docker Compose templates, new environments spin up in hours instead of the 2–3 day manual process.
The real ROI isn't in the installation itself—it's in the incidents that never happen. Every configuration decision above—from NoNewPrivileges in systemd to admin_passwd = False in odoo.conf—removes a failure mode that would otherwise cost you hours of downtime, support tickets, or worse: a security breach that erodes customer trust.
Optimization Metadata
Production-ready Odoo 19 installation via Docker and source. Covers PostgreSQL tuning, security hardening, Nginx config, and backup strategy.
1. "Docker vs Source vs Odoo.sh: Which Odoo 19 Installation Method Fits Your Team"
2. "Production-Grade Docker Compose for Odoo 19: The Full Stack"
3. "3 Installation Mistakes That Cripple Odoo 19 in Production"