Skip to content

Latest commit

 

History

History
234 lines (173 loc) · 9.1 KB

File metadata and controls

234 lines (173 loc) · 9.1 KB

DESIGN.md — Odoo Infrastructure

Overview

Production-ready Docker Compose stack for Odoo 18, built to run reliably on a single server today and scale to 500+ customers tomorrow. This document explains what was chosen and why since every decision below has a tradeoff


Architecture

Internet
   │
   ▼
[Nginx :443]  ← TLS termination, security headers, rate limiting
   │
   ▼
[Odoo :8069 / :8072]  ← app workers + gevent longpolling
   │
   ▼
[PostgreSQL :5432]  ← data layer, internal network only

Exporters (metrics, connected to a centralised Monitoring Host {Prometheus & Grafana}):
  node-exporter :9100  →  host metrics
  postgres-exporter :9187  →  DB metrics

Networks:

  • frontend — nginx ↔ odoo only
  • backend — odoo ↔ postgres ↔ backup only

PostgreSQL is never reachable from outside the backend network. Nginx is the only internet-facing entry point.


Security

Container hardening

All containers run with cap_drop: [ALL] and no-new-privileges:true. Capabilities are added back only where required:

  • Nginx gets NET_BIND_SERVICE + CHOWN (needed to bind ports 80/443 and set cache dir ownership)

Secrets

Credentials are passed as environment variables for this deployment. In production, these would be injected by a secrets manager (HashiCorp Vault, AWS Secrets Manager) at deploy time via CI/CD. Never stored in the compose file or image.

Odoo-specific

  • list_db = False — hides the database selector from the login page, preventing enumeration
  • proxy_mode = True — correct IP handling behind Nginx
  • /web/database/ blocked at Nginx level (configured, pending SSL setup)
  • TLS 1.2+ only, HSTS enabled — pending production certificate
  • Login endpoint rate-limited to 5 requests/minute

TLS

Production path: Certbot + Let's Encrypt with automated renewal via cron. TLS 1.2+ only, strong cipher suite, HSTS enabled.

Network isolation

Postgres port 5432 is never published to the host. Only Nginx ports 80/443 are internet-facing. Even if a firewall rule is misconfigured, Odoo port 8069 is bound to the internal network only.


Data Persistence & Backup

Volumes

Named Docker volumes (not bind mounts) for all stateful data:

  • pg_data — PostgreSQL database
  • odoo_data — Odoo filestore (attachments, documents)

Backup strategy

A dedicated backup container runs daily at 02:00:

  1. pg_dump → compressed .sql.gz
  2. Filestore tar → .tar.gz
  3. Files timestamped and stored in backup_data volume
  4. Backups older than 30 days deleted automatically
  5. Weekly restore test

RPO: 24 hours (daily backup)
RTO: ~15 minutes (restore DB + filestore + restart)

Restore procedure

# 1. Restore database
docker exec -i odoo-db psql -U odoo odoo < backup_YYYY-MM-DD.sql

# 2. Restore filestore
docker run --rm -v odoo_data:/var/lib/odoo \
  -v $(pwd)/backups:/backups alpine \
  tar -xzf /backups/filestore_YYYY-MM-DD.tar.gz -C /var/lib/odoo

# 3. Restart Odoo
docker compose restart odoo

Restore procedure was manually verified during development.

Production evolution

For production: pipe pg_dump directly to GCS (commented in compose), use Restic for encrypted deduplicated off-site backups. Retention policy: daily for 30 days, weekly for 3 months.


Observability

Local observability (docker-compose.monitoring.yml)

A companion docker-compose.monitoring.yml is included for local development and demo use. Prometheus and Grafana run alongside the main stack and communicate via a shared external Docker bridge network (odoo-monitoring).

This is not the production architecture — it exists to make the stack self-contained for demos and local testing. In production, the exporters are scraped by a central Prometheus server over VPN.

To run locally:

docker compose -f docker-compose.yml -f docker-compose.monitoring.yml up -d

Prodcution setup (exporters-only)

This stack exposes metrics endpoints for scraping by a centralised observability platform:

Exporter Port Metrics
node-exporter 9100 CPU, memory, disk, network
postgres-exporter 9187 connections, cache hit ratio, vacuum, slow queries

Centralised architecture (production)

At scale, each Odoo instance runs exporters only. A central Prometheus server scrapes all instances. A central Grafana provides dashboards for all customers. This avoids running a Prometheus per customer (which would be unmanageable at 500 instances).

Recommended dashboards: Node Exporter Full (ID 1860), PostgreSQL (ID 9628).

What to monitor and why

  • PG connection count — Odoo opens one connection per worker. At 80% of max_connections, new workers can't start and requests fail silently.
  • Disk usage — full disk causes silent write failures in both Postgres and the filestore.
  • Memory — OOM kills happen without warning. Alert at 90% of container limit.
  • Backup age — a backup container that's running but silently failing is worse than no backup.

Option B — Separate compose on same host A second docker-compose.monitoring.yml runs Prometheus + Grafana on the same server. The two stacks communicate via a shared external Docker network:


Silent Failure Handling

Stack addresses silent failutes in three layers.

Startup dependencies depends_on: condition: service_healthy ensures Odoo never starts until Postgres passes pg_isready -U odoo -d odoo.

Runtime healthchecks

  • Odoo alive but DB disconnected — /web/health returns 500, Docker restarts the container
  • DB process up but not accepting queries — pg_isready -d odoo checks the specific database, not just the process
  • Backup running but producing nothing — healthcheck verifies a backup file exists and is less than 24h old
  • Nginx misconfigured after restart — nginx -t validates config syntax on every healthcheck interval

Resource and log limits

  • Disk full → silent write failures — node-exporter metrics with alert at 80% usage
  • Memory exhaustion → OOM kill — container limits enforced, alert at 90% of limit
  • Log files filling disk — json-file driver, 50MB max, 5 files per container
  • Stuck Odoo workers — limit_memory_hard = 2.5GB and limit_time_real = 120s in odoo.conf, hard kill on breach

Alert rules monitoring/alerts.yml contains reference Prometheus alert rules covering failure scenarios above — ContainerDown, HighMemory, DiskAlmostFull, PostgresHighConnections, BackupMissed, OdooHighCPU.

This file lives on the central Prometheus server. Prometheus evaluates the rules and fires alerts via Alertmanager to Slack or PagerDuty. Included here as documentation for the monitoring team responsible for the central observability platform.


Scalability Path

Now — single server, Docker Compose (~50 customers)

This stack. One server, vertical scaling, simple operations. Works well up to the point where Postgres becomes the bottleneck.

~100 customers

  • Move Postgres to a managed service (Cloud SQL) — automatic failover, daily snapshots, point-in-time recovery out of the box
  • Keep Odoo on Docker Compose but increase worker count vertically

~200 customers

  • Migrate Odoo to Kubernetes + Helm chart
  • Horizontal pod autoscaling based on CPU/memory
  • Shared session storage via Redis

500+ customers

  • Each customer gets their own database on a shared Postgres cluster — isolates performance and data between tenants
  • Odoo file attachments move to GCS — required when multiple Odoo servers need to access the same files simultaneously
  • CDN (e.g. Cloudflare) serves static assets — reduces server load and improves response times globally
  • Multi-region deployment with data replication — eliminates single datacenter as a point of failure

Known Limitations & What to Add Next

Credentials Plain credentials in compose. Production path: injected at deploy time via Vault or Secrets Manager through CI/CD.

TLS Self-signed cert for local development. Production: Certbot + Let's Encrypt with automated renewal via cron or cert-manager.

Alerting Prometheus alert rules are defined but Alertmanager is not wired up. Next step: Connect to Slack webhook for on-call notifications.

Observability Exporters running and verified. A local docker-compose.monitoring.yml is included for demo use. Production path: central Prometheus server scraping all instances via VPN/Tailscale.

Redis / Session storage Not included in this single-server setup. Required when scaling horizontally: multiple Odoo containers need a shared session store, otherwise users get logged out when requests hit different instances. Redis becomes mandatory before horizontal scaling is possible.

Kubernetes migration Docker Compose is the right tool for a single server. The natural next step is a Helm chart — same concepts, adds autoscaling, rolling deployments, and self-healing at the infrastructure level.

Operational Notes

First boot — database requires one-time initialisation:

docker compose run --rm odoo bash
/usr/bin/odoo -i base --db_host=db --db_user=odoo --db_password=odoo \
  --database=odoo --without-demo=all --stop-after-init