I've been running Odoo in Docker containers for years. Single instance, simple, predictable. Then one Friday a DNS change hit the wrong server and took down three client-facing websites for forty minutes. That was the push I needed to go multi-replica.
This is the story of running Odoo 19 across a Docker Swarm cluster — what worked, what broke, and the patterns that survived production.
The Setup
Three VPS nodes. One manager, two workers. Traefik as reverse proxy, routing HTTPS to Odoo across the overlay network. The manager also runs PostgreSQL and an NFS server.
The naive approach: scale Odoo to 2 replicas and let Swarm handle the rest. The real approach: a week of debugging edge cases nobody writes about.
The Infinite Login Loop
First thing that broke. After deploying with 2 replicas, users could type their password correctly and get... the login page again. And again. A perfect 303 redirect loop.
Root cause: Odoo stores sessions on the local filesystem. Replica 1 creates a session file. Traefik round-robins the next request to Replica 2, which doesn't have it. Back to login.
We went with Redis + sticky cookies as a belt-and-suspenders fix. The OCA module session_redis from Camptocamp replaces Odoo's filesystem session store with a shared Redis instance on the overlay network.
# odoo.conf
server_wide_modules = web,session_redis
Add redis:7-alpine to your stack on the same network — the module auto-discovers it via Swarm DNS. Then add Traefik sticky cookies as a safety net: requests stay pinned to one replica for warm caches, but if it dies, the session is still valid in Redis.
The Vanishing Filestore
Odoo's filestore holds compiled assets, documents, product images — everything. With local Docker volumes, each node has its own copy. Upload an image on Worker 1? Worker 2 serves a 500.
The fix: NFS shared storage. Export a directory from the manager, mount it as a Docker volume on all workers. One gotcha: Docker Swarm does NOT propagate docker volume create across nodes. Define the NFS mount in the service's --mount flag and let Swarm handle it.
The Database That Walked Away
Swarm reschedules services across nodes when things go wrong. For databases, that's catastrophic. Our PostgreSQL container got moved to a different node during a rebalance. The data volume stayed behind. database "19.0" does not exist.
Fix: pin stateful services with a hard constraint.
docker service update --constraint-add "node.hostname==<db-node>" website_db
Zero-Downtime Module Updates
You can't update Odoo modules while serving requests — database locks conflict. The pattern:
- Scale to zero
- Spin up a temporary service with
--restart-condition noneon the same network - Run
odoo -u <module> --stop-after-init --no-http - Remove temp service, restore replicas
Wrap it in a trap that restores replicas on failure. We once had a script crash mid-update, leaving the service at 0 replicas for an hour.
And never -u all with enterprise modules. We hit an xpath error at module 148 of 234. Update only what you need.
The Dependency Trap
Adding a dependency to __manifest__.py that isn't installed in production doesn't just break your module — Odoo silently skips it and everything depending on it. Tables stay intact, but every model, cron, and controller vanishes.
We learned this the hard way with sale_subscription. Always verify dependencies exist in prod before merging.
The Survival Checklist
- Redis for sessions + sticky cookies as safety net
- NFS for filestore — shared across all nodes
- Pin databases to their data node
- Never
-u all— update specific modules - Deploy scripts must restore replicas on failure (
trap) - All domains in Traefik rules — a missing domain is a silent 404
- Test manifest dependencies against the production DB before merging
docker pullbeforeservice update— Swarm caches digests aggressively- Start-first update order — new container healthy before old one dies
Is It Worth It?
For most Odoo deployments, a single instance with good backups is enough. But if you're serving multiple websites, running crons that can't afford gaps, or tired of 2 AM downtime messages — the investment pays off.
The key insight: Odoo wasn't designed for horizontal scaling. Every solution here is a workaround for baked-in assumptions — local sessions, local filestore, tight DB coupling. Knowing that upfront saves you from expecting it to "just work."