Odoo high availability on Docker Swarm means three different things, and each fails in its own way: the Swarm control plane (manager quorum), the Odoo application tier (replicas spread across nodes), and the data tier (PostgreSQL and the filestore). Swarm handles the first two well. It does nothing for the third. This guide covers manager quorum, anti-affinity for Odoo replicas, how to size replicas correctly, healthchecks and safe updates, and what you have to solve outside Swarm.
For the base cluster setup, start with the Odoo on Docker Swarm pillar guide.
How much availability you need
| Availability | Downtime per year | Downtime per month |
|---|---|---|
| 99% | 87.6 hours | 7.3 hours |
| 99.9% | 8.76 hours | 43.8 minutes |
| 99.99% | 52.6 minutes | 4.4 minutes |
Be honest about the target. Most Odoo deployments are used during business hours by one company, and a planned 10-minute window for a module upgrade is acceptable. What hurts is unplanned downtime: a dead node at 10 a.m., a stuck worker, an update that never finishes. That is what the patterns below protect against.
Swarm manager quorum
Swarm managers keep the cluster state with the Raft consensus algorithm. Any change (deploying a stack, scaling, rescheduling a failed task) needs a majority of managers to agree. With N managers, the majority is floor(N/2) + 1:
| Managers | Majority needed | Failures tolerated |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
Use an odd number. Four managers tolerate one failure, exactly like three, but every write has to reach one more node. Two managers are worse than one: losing either stops the control plane. For most Odoo clusters, three managers are right; five make sense once you run many nodes or want to survive two failures. Every extra manager slows writes a little, because more nodes must acknowledge each change.
What happens when quorum is lost
Running Odoo containers keep running and keep serving users. But nothing can be changed: no deploys, no scaling, and failed tasks are not rescheduled onto other nodes. A node failure during a quorum loss therefore becomes a real outage.
If the missing managers can come back, bring them back and quorum returns on its own. If they are gone for good, rebuild from one surviving manager:
docker swarm init --force-new-cluster --advertise-addr <this-node-ip>:2377
docker node promote <node-2> <node-3>
The new single-manager cluster keeps services and state from that manager. Promote other nodes right away to get back to three managers.
Placing managers
- Spread managers across at least three failure domains (availability zones, racks, or hosting locations): 1-1-1 for three managers, 2-2-1 for five. With only two locations, one of them always holds the majority, and losing that location loses quorum.
- Keep latency between managers low. Raft over a slow or flaky link causes leader elections and slow cluster operations.
- Keep Odoo off the managers so a busy Odoo worker cannot starve Raft:
docker node update --availability drain <manager>. Small clusters can run workloads on managers, but put resource limits on everything. - Back up
/var/lib/docker/swarmfrom a manager (with Docker stopped on that node), and keep the autolock unlock key if you use autolock.
Running Odoo with more than one replica
A single Odoo replica on Swarm already gets some protection: if its node dies, Swarm starts a new task elsewhere. That takes as long as a cold Odoo boot plus the healthcheck, usually one to a few minutes. To get rid of that gap, run two or more replicas. Odoo supports this, with three requirements:
- A shared filestore. Odoo stores attachments and user sessions under its data directory. Every replica must mount the same filestore, or users get logged out and attachments go missing depending on which replica answers. See shared filestore storage for Odoo on Swarm.
- Identical code. Same image, same addons, same
odoo.confon every replica. - Correct routing. The proxy must send normal requests to port 8069 and
/websocketto port 8072 of every replica. Odoo's bus uses PostgreSQL notifications, so a websocket on one replica still receives messages caused by another.
Scheduled actions are safe with several replicas: each Odoo process with cron threads claims jobs with FOR NO KEY UPDATE SKIP LOCKED, so a job never runs twice at the same time.
Inside each replica, capacity comes from Odoo workers. Replicas and workers are different levers; the guide to scaling Odoo covers when to add which.
How many replicas: the formula
Size replicas from two numbers: how many you need to carry normal load, and how many you are willing to lose at once.
replicas = replicas needed for load + replicas you can lose
Example: peak load needs three Odoo replicas (say 3 x 4 workers), and you want to survive one node failure. With at most one replica per node, a node failure costs one replica, so you need 3 + 1 = 4 replicas on at least 4 nodes. A common mistake is to add one more on top of that ("+1 for safety"). That counts the spare twice and gives 5, which pays for capacity you did not ask for.
The formula only holds when replicas are actually spread. Four replicas packed onto two nodes lose two at once, which is what anti-affinity prevents.
Anti-affinity: keep replicas apart
Swarm spreads tasks of a service across nodes by default, but it does not promise to. Make it a rule:
odoo:
image: odoo:19
deploy:
replicas: 4
placement:
max_replicas_per_node: 1
constraints:
- node.labels.role == odoo
preferences:
- spread: node.labels.zonemax_replicas_per_node: 1is real anti-affinity: Swarm never places two Odoo tasks on one node. If there are fewer eligible nodes than replicas, the extra tasks wait in Pending, which is exactly what you want to notice. The CLI equivalent is--replicas-max-per-node 1.spread: node.labels.zonebalances tasks across zones first. Nodes without the label are treated as one more group, so label every node.- Label nodes with
docker node update --label-add zone=a node-1, and so on.
For services that should run once on every node, such as a monitoring agent or a reverse proxy publishing host-mode ports, use mode: global instead. The guide to Odoo server placement covers constraints and node labels in depth.
Healthchecks that catch a stuck Odoo
A running container is not a working Odoo. Workers can deadlock under load while the process stays alive. Odoo 17 and later answer /web/health without a database lookup, and the official image ships Python, so no curl is needed:
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8069/web/health', timeout=8).status == 200 else 1)"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
stop_grace_period: 300sstart_periodgives Odoo time to boot and load the registry before failures count. Large databases need more.retries: 5at 30 seconds means about 2.5 minutes of failure before Swarm replaces the task, so a short load spike does not trigger restarts that make things worse.stop_grace_periodmatters more for Odoo than for most apps. Docker's default is 10 seconds. If a task is stopped in the middle of a module installation, the module can be left half-installed, and the database then logs errors on every start. Give it minutes, not seconds.
Add ?db_server_status=1 to the URL to also check the PostgreSQL connection, but think before using that for the healthcheck: if PostgreSQL goes down, every Odoo replica will be restarted in a loop, which does not help.
Updates without downtime
deploy:
update_config:
parallelism: 1
order: start-first
failure_action: rollback
monitor: 180s
rollback_config:
order: start-firstorder: start-firststarts the new task and waits for it to be healthy before stopping the old one, so there is never zero running tasks.monitormust be longer thanstart_periodplus one interval. If it is shorter, Swarm declares the update finished before the healthcheck has said anything.- Do not use start-first on a service that publishes host-mode ports: two tasks cannot bind the same port on one node, and the update never completes.
One Odoo-specific limit: a rolling update briefly runs old and new code against the same database. That is fine for code-only changes. A module upgrade (-u) changes the database schema, so run it as a one-off task in a short maintenance window instead of letting two versions race. The zero-downtime deployment guide covers the procedure.
The hard part: PostgreSQL and the filestore
Swarm can restart a PostgreSQL container on another node, but the data does not move with it. A database on a node's local disk is pinned to that node, and if the node dies, Odoo is down until it comes back or you restore a backup. Realistic options:
- Single PostgreSQL with strong backups. Pin it to one well-sized node with a placement constraint, take frequent backups off the node, and practice restores. Downtime on failure equals your restore time. This is what most small and mid-size Odoo deployments run.
- Replicated PostgreSQL outside the Odoo stack. A managed PostgreSQL service with failover, or a primary/standby cluster with automatic failover, run by people who know it. Odoo connects to one stable address.
The filestore has the same problem. If it sits on one NFS server, that server is your single point of failure. Pick storage whose availability matches the rest of the design, and back up the filestore together with the database: one without the other cannot be restored to a consistent state.
Test it before it happens
# Move all tasks off a node, as if it failed
docker node update --availability drain node-3
docker service ps erp_odoo
# Bring it back
docker node update --availability active node-3
While a node is drained, users should notice nothing. Also try stopping a manager to confirm the remaining managers keep quorum, and restore your latest database backup to a test stack. If any of these surprise you, they will surprise you more in production.
Easy Instance runs each Odoo instance on Docker Swarm with healthchecks, start-first updates with automatic rollback, and daily backups you can restore from the dashboard. If you would rather not run quorum math yourself, compare pricing or start a 10-day free trial, no credit card needed.