This guide shows how to deploy an Odoo Docker stack on Swarm: Odoo 19 and PostgreSQL 16 defined in one stack file, the database password kept in a Swarm secret, HTTPS from Traefik v3, and rolling updates that roll back on their own if something fails. Every command uses current Docker syntax. The stack file is plain Compose-spec YAML, so if you already run Odoo with Docker Compose, most of it will look familiar.
Before you start
- A Swarm with at least one manager (
docker swarm init). To add nodes, see Docker Swarm cluster for Odoo. - A DNS record (for example
odoo.example.com) pointing to the node that will run Traefik. Ports 80 and 443 must be open on that node. - Run every command below on a manager node.
Prepare the Swarm: network, secret and node labels
Create an overlay network that Traefik and Odoo will share:
docker network create --driver overlay --attachable traefik-public
Store the database password as a Swarm secret. Secrets are encrypted in the Swarm's Raft store and only mounted into containers that ask for them, under /run/secrets/:
openssl rand -base64 24 | tr -d '\n' | docker secret create odoo_db_password -
This stack uses local volumes, which stay on one node. So pin PostgreSQL and Odoo to the node that holds their data:
docker node update --label-add odoo.data=true <node-name>
If you want Odoo to move freely between nodes, put its filestore on shared storage instead. See Odoo filestore on NFS.
Deploy Traefik v3 as the entry point
Traefik v3 has a dedicated swarm provider. In v2 this was the docker provider with swarmMode=true, and older tutorials still show that. Traefik needs the Docker API of a manager, so it is constrained to managers. Its ports use host mode so Odoo sees the real client IP.
services:
traefik:
image: traefik:v3.3
command:
- --providers.swarm=true
- --providers.swarm.exposedByDefault=false
- --providers.swarm.network=traefik-public
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
networks:
- traefik-public
deploy:
placement:
constraints:
- node.role == manager
networks:
traefik-public:
external: true
volumes:
letsencrypt:export ACME_EMAIL=you@example.com
docker stack deploy -c traefik.yml traefik
docker stack deploy substitutes ${VARIABLES} from your shell environment. Unlike docker compose, it does not read a .env file automatically. Mounting the Docker socket gives Traefik full control of the Docker API. On shared clusters, put a socket proxy in front of it.
Write odoo.conf for a proxied, multi-worker Odoo
[options]
db_host = db
db_port = 5432
db_user = odoo
db_name = odoo
dbfilter = ^odoo$
list_db = False
proxy_mode = True
data_dir = /var/lib/odoo
addons_path = /mnt/extra-addons
workers = 2
max_cron_threads = 1
gevent_port = 8072The file has no password. The official Odoo image reads PASSWORD_FILE at startup and passes the password to Odoo, so the secret never appears in the config. proxy_mode makes Odoo trust Traefik's forwarded headers. list_db = False hides the database manager. With workers above 0, Odoo serves its websocket on gevent_port, which is why Traefik needs a second route below. For sizing workers and memory limits, see Odoo server configuration.
The Odoo swarm stack file
services:
db:
image: postgres:16
environment:
POSTGRES_USER: odoo
POSTGRES_DB: postgres
POSTGRES_PASSWORD_FILE: /run/secrets/odoo_db_password
secrets:
- odoo_db_password
volumes:
- pg-data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U odoo"]
interval: 10s
timeout: 5s
retries: 5
deploy:
placement:
constraints:
- node.labels.odoo.data == true
restart_policy:
condition: any
odoo:
image: odoo:19.0
environment:
PASSWORD_FILE: /run/secrets/odoo_db_password
secrets:
- odoo_db_password
configs:
- source: odoo_conf
target: /etc/odoo/odoo.conf
volumes:
- odoo-data:/var/lib/odoo
networks:
- backend
- traefik-public
stop_grace_period: 120s
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request as u,sys;sys.exit(0 if u.urlopen('http://localhost:8069/web/health',timeout=8).getcode()==200 else 1)"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
deploy:
replicas: 1
placement:
constraints:
- node.labels.odoo.data == true
restart_policy:
condition: any
delay: 5s
update_config:
order: start-first
failure_action: rollback
monitor: 180s
rollback_config:
order: start-first
resources:
limits:
cpus: "2"
memory: 3g
reservations:
cpus: "0.5"
memory: 1g
labels:
- "traefik.enable=true"
- "traefik.http.routers.odoo.rule=Host(`odoo.example.com`)"
- "traefik.http.routers.odoo.entrypoints=websecure"
- "traefik.http.routers.odoo.tls.certresolver=le"
- "traefik.http.routers.odoo.service=odoo"
- "traefik.http.services.odoo.loadbalancer.server.port=8069"
- "traefik.http.routers.odoo-ws.rule=Host(`odoo.example.com`) && PathPrefix(`/websocket`)"
- "traefik.http.routers.odoo-ws.entrypoints=websecure"
- "traefik.http.routers.odoo-ws.tls.certresolver=le"
- "traefik.http.routers.odoo-ws.service=odoo-ws"
- "traefik.http.services.odoo-ws.loadbalancer.server.port=8072"
networks:
backend:
driver: overlay
traefik-public:
external: true
volumes:
pg-data:
odoo-data:
secrets:
odoo_db_password:
external: true
configs:
odoo_conf:
name: odoo_conf_v1
file: ./odoo.confThings that differ from a Compose file:
- Traefik labels go under
deploy.labels(service labels), not top-levellabels. In Swarm mode Traefik also needs theloadbalancer.server.portlabel. depends_on,build,container_nameandrestartare ignored bydocker stack deploy. The Odoo image's entrypoint already waits for PostgreSQL. If it gives up, Swarm restarts the task.- There is no top-level
version:key. The Compose specification doesn't use it. - The health check uses Python because the Odoo image always includes it.
start-firstplus that health check means a new Odoo container must answer/web/healthbefore the old one stops.
Deploy the Odoo docker stack
docker stack deploy -c odoo.yml --detach=false odoo
--detach=false (Docker 26 and later) waits until the services converge, so errors show up right away. Add --with-registry-auth if your Odoo image is in a private registry. Then create the database once. The command runs inside the Odoo container on the node that holds it:
docker exec -it $(docker ps -q -f name=odoo_odoo) \
sh -c 'odoo -c /etc/odoo/odoo.conf -d odoo -i base \
--db_password="$(cat /run/secrets/odoo_db_password)" \
--workers=0 --no-http --stop-after-init'
Open https://odoo.example.com and log in as admin with password admin, then change that password immediately. To keep the master password (admin_passwd) out of plain config, see Odoo docker secrets in Swarm.
Check the stack
docker stack services odoo
docker service ps odoo_odoo --no-trunc
docker service logs -f --tail 100 odoo_odoo
docker service ps shows each task's node, state and full error message, for example no suitable node when a placement label is missing. For deeper troubleshooting, see troubleshoot Odoo on Docker Swarm.
Update Odoo, config and resources
- New image or limits: edit
odoo.ymland run the samedocker stack deploycommand again. Only services whose definition changed are updated. - New odoo.conf: Swarm configs can't be changed once created. Bump the name (
odoo_conf_v2) and redeploy. The rolling update swaps the file in. - Module upgrades: run
odoo -u <module>the same way as the database creation above, then restart withdocker service update --force odoo_odoo. - Back out a bad change:
docker service rollback odoo_odoo.
Remove the stack
docker stack rm odoo
This removes the stack's services, its backend network and the configs it created. Named volumes (pg-data, odoo-data) and the external secret are kept. Back them up before deleting anything by hand.
When you would rather not run the stack
Easy Instance runs each customer's Odoo as its own Swarm stack using these patterns. How Easy Instance runs Odoo on Docker Swarm explains the setup. If you want the result without operating a cluster, compare pricing or start a 10-day free trial, no credit card needed.