Odoo zero downtime deployment on Docker Swarm is possible for some changes but not for others. A new image with code fixes, a new Python package or a config change can roll out while users keep working. A module upgrade (-u) that changes the database cannot, no matter how you configure the rollout. This guide covers how Swarm rolling updates work, why start-first matters for an Odoo service, how automatic rollback really works, and the safe way to handle module upgrades.
How a Swarm rolling update works
When you change a service (a new image tag, an environment variable, a resource limit), Swarm replaces its tasks in batches rather than all at once. Out of the box, docker service update uses these settings:
| Setting | CLI flag | Default | Meaning |
|---|---|---|---|
| Parallelism | --update-parallelism | 1 | Tasks replaced at a time |
| Delay | --update-delay | 0s | Wait between batches |
| Order | --update-order | stop-first | Stop old task before starting new one |
| Monitor | --update-monitor | 5s | How long to watch each new task for failure |
| Failure action | --update-failure-action | pause | pause, continue or rollback |
| Max failure ratio | --update-max-failure-ratio | 0 | Share of failed tasks tolerated |
Note the default order: stop-first. For an Odoo service with one replica, that means Swarm stops the only running container, then starts the new one. Users get errors until the new container has loaded Odoo's registry, which can take from a few seconds to over a minute on a database with many modules.
start-first vs stop-first for Odoo
With order: start-first, Swarm starts the new task, waits until it is running (and healthy, if the service has a health check), and only then stops the old one. Old and new containers briefly run side by side against the same database and filestore. That is what makes a code-only update seamless for users.
Use start-first when:
- The change is code or configuration only, with no module upgrade.
- Your nodes have the spare RAM for one extra Odoo container during the update.
- Traffic reaches Odoo through a proxy such as Traefik. If you publish Odoo's port in
hostmode, the new task cannot bind a port the old task on the same node still holds, and the update stalls.
Keep stop-first when two copies must never run at once, for example if the new image runs an upgrade on startup (do not do that, see below), or when memory is too tight for an extra container.
Add a health check first
Without a health check, Swarm treats a container as ready as soon as its process starts, long before Odoo can answer requests. start-first then hands traffic to a container that is still loading. Odoo 17 and later expose /web/health:
odoo:
image: registry.example.com/odoo-custom:18.0-20261110
command: ["--workers=4", "--proxy-mode"]
healthcheck:
test: ["CMD", "curl", "-fs", "http://localhost:8069/web/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 90s
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 20s
order: start-first
monitor: 60s
failure_action: rollback
max_failure_ratio: 0
rollback_config:
parallelism: 0
order: start-first
monitor: 30s
failure_action: pausemonitor: 60s keeps Swarm watching each new task for a minute after it starts. If the task exits or turns unhealthy in that window, the update counts as failed and failure_action: rollback takes over. Set start_period longer than your slowest Odoo start, or the health check kills containers that are still loading.
How rollback works (and which settings it uses)
A rollback does not reuse update_config. It follows its own rollback_config block, or the --rollback-* flags on the CLI. Its defaults are the same as for updates: parallelism 1, order stop-first, failure action pause. If you only set order: start-first under update_config, an automatic rollback still stops the new task before starting the old one, and your users see the downtime you configured the update to avoid. Set both blocks.
In the example above, parallelism: 0 rolls every task back at once. That is usually what you want when the new image is broken: get the old version back as fast as possible.
To roll back by hand, for example when the new image is healthy but has a bug, run either of these:
docker service rollback odoo_odoo
# same effect
docker service update --rollback odoo_odoo
Both revert to the service spec in place before the most recent update. There is only one step of history, so rolling back twice returns you to the version you just left.
Ship a new Odoo image
Build images with an explicit, unique tag (a date, a Git SHA). Never deploy :latest. Swarm cannot tell that a moving tag changed, and you cannot roll back to a tag that now points at something else. Then either update the tag in the stack file and redeploy:
docker stack deploy -c odoo-stack.yml --with-registry-auth odoo
or update the service directly:
docker service update \
--image registry.example.com/odoo-custom:18.0-20261111 \
--with-registry-auth \
odoo_odoo
Watch the rollout with docker service ps odoo_odoo. docker service inspect --pretty odoo_odoo shows the update status and the current update and rollback settings. There is no flag to pause or resume an update. An update that stopped because of failure_action: pause stays paused until you run another docker service update (for example with a fixed image) or roll back.
For a custom image with your addons baked in, see building an Odoo Docker image.
Why Odoo module upgrades are not zero-downtime
Running odoo -u my_module changes the database: it adds or drops columns, rewrites records, reloads views and recomputes stored fields. While that runs:
- Old containers still serve requests with code that expects the old schema. Users hit errors as soon as a column they need changes.
- The upgrade takes locks on the tables it changes, so requests wait or time out.
- When it finishes, every running worker must reload its registry before it matches the new database.
A rolling update cannot fix this, because every replica shares one database. Worse, never put -u in the service command: with two replicas and start-first, several containers would run the same upgrade against the same database at the same time.
Treat module upgrades as a short maintenance window:
- Test the upgrade on a copy of production first (Odoo staging environment).
- Take a database and filestore backup (Odoo backup).
- Stop the web service:
docker service scale odoo_odoo=0. - Run the upgrade once, as a one-off job with the new image:
docker service create --name odoo-upgrade \
--mode replicated-job \
--network odoo_internal \
--env-file odoo-db.env \
--mount type=volume,source=odoo_odoo-data,target=/var/lib/odoo \
registry.example.com/odoo-custom:18.0-20261111 \
odoo -d production -u my_module --stop-after-init
docker service logs -f odoo-upgrade
docker service rm odoo-upgrade
- Point the stack at the new image and scale back up:
docker stack deploy -c odoo-stack.yml odoo.
odoo-db.env holds the HOST, USER and PASSWORD variables the official image uses to connect, with HOST=odoo_db (the stack's database service). The mount reuses the stack's data volume so the upgrade can write attachments and assets. On a multi-node swarm, that volume must be shared storage or the job must land on the node that holds it.
Which changes can roll out live
| Change | Rolling update with start-first? |
|---|---|
| Bug fix in Python code, no model changes | Yes |
| New Python dependency in the image | Yes |
odoo.conf or environment change | Yes |
| New field, model or view in a module | No, needs -u |
| Odoo minor update with module data changes | No, needs -u |
| Major version upgrade (17 to 18) | No, needs a migration |
On Easy Instance you test the same flow without running a swarm: deploy custom modules from GitHub, try the upgrade on a staging copy, then apply it to production. It costs $30 per Odoo worker per month (pricing), and the 10-day free trial needs no credit card.