When you troubleshoot Odoo on Docker Swarm, the same handful of failures come up again and again: tasks that restart in a loop, tasks stuck in Pending, Odoo that cannot reach PostgreSQL, healthchecks that kill a working instance, and websockets that fail with a 400 error. This guide gives you the commands to find out which one you have and the fix for each, using an Odoo stack deployed as erp (services erp_odoo and erp_db).
Run the commands on a manager node unless noted otherwise. For how the stack is built, see the Odoo on Docker Swarm pillar guide.
Step 1: Check cluster and service status
Start wide. Two commands tell you whether the problem is a node or a service:
docker node ls
docker service ls
In docker node ls, look at three columns:
- STATUS:
ReadyorDown. A Down node's tasks are rescheduled elsewhere if there is room. - AVAILABILITY:
Active,Pause(keeps running tasks, takes no new ones) orDrain(no tasks at all). A node someone forgot to reactivate after maintenance is a common cause of Pending tasks. - MANAGER STATUS:
Leader,ReachableorUnreachable. An unreachable manager reduces your margin before quorum is lost.
In docker service ls, the REPLICAS column shows running versus desired tasks. 1/1 is healthy; 0/1 means Odoo is down. To list only the services that are not at their desired count:
docker service ls --format '{{.Name}} {{.Replicas}}' \
| awk '{split($2, r, "/"); if (r[1] != r[2]) print}'
Useful filters: docker node ls --filter role=manager, docker service ls --filter name=erp_, and docker node ps <node> to see every task on one node.
Step 2: Read the task history
Every restart creates a new task, and Swarm keeps the recent ones. This is where most answers are:
docker service ps erp_odoo --no-trunc
Without --no-trunc, the ERROR column is cut off, and it usually holds the exact reason. To see only the current task, or a compact history:
docker service ps erp_odoo --filter desired-state=running
docker service ps erp_odoo --format '{{.Name}} {{.Node}} {{.CurrentState}} {{.Error}}'
For a single task, docker inspect gives the state, the error and the container's exit code:
docker inspect --format '{{.Status.State}} | {{.Status.Err}} | exit {{.Status.ContainerStatus.ExitCode}}' <task-id>
And read the log of the service, or of one specific failed task:
docker service logs erp_odoo --since 30m --timestamps --tail 300
docker service logs <task-id>
Odoo tasks restarting in a loop (task churn)
If docker service ps shows a new task every minute or two, match the exit code and the last log lines:
| Symptom | Likely cause | Fix |
|---|---|---|
| Exit 1 within seconds, Python traceback in the log | Odoo cannot start: bad odoo.conf, wrong addons_path, a custom module that fails to import, a missing Python dependency | Read the traceback; fix the config or the module; redeploy |
| Exit 1 after about 30 seconds, log mentions the database | The image's entrypoint waits up to 30 seconds for PostgreSQL, then gives up | See Odoo cannot reach PostgreSQL |
| Exit 137 | The container was killed: out of memory against its Swarm limit, or stopped and not finished within stop_grace_period | Check memory in docker stats; size limit_memory_hard times processes below the container limit; raise the limit |
| Task error says the container is unhealthy | The healthcheck failed enough times in a row | See Healthchecks |
PermissionError on /var/lib/odoo | The filestore volume is not writable by the odoo user in the container (common with NFS and bind mounts) | Fix ownership on the storage side |
| "Some modules have inconsistent states" on every start | A module install or upgrade was killed halfway, often by a healthcheck restart or a short stop_grace_period | Finish or roll back the module from a one-off Odoo shell, then raise stop_grace_period to several minutes |
Stop the churn first. Each restart buries the useful log further. Roll back the last change, or scale to zero while you investigate:
docker service rollback erp_odoo
docker service scale erp_odoo=0
If the stack sets restart_policy.max_attempts, Swarm eventually stops trying and leaves the service at 0/1 with every task Failed. After fixing the cause, start it again with docker service update --force erp_odoo.
Tasks stuck in Pending
A Pending task means the scheduler cannot find a node for it. docker service ps erp_odoo --no-trunc tells you why:
no suitable node (insufficient resources on N nodes): the service'sreservationsdo not fit. Swarm schedules on reservations, not real usage. Check what a node offers withdocker node inspect <node> --format '{{json .Description.Resources}}', then lower the reservation or add a node.scheduling constraints not satisfied: no node matches theplacement.constraints. Usually a missing label (docker node update --label-add role=odoo <node>) or a drained node.host-mode port already in use: the service publishes a port in host mode and another task already holds it on that node. Withupdate_config.order: start-firstthis blocks every update forever, because old and new tasks cannot share the port. Usestop-firstfor host-mode services.- Max replicas per node reached:
max_replicas_per_nodeis set and there are fewer eligible nodes than replicas.
A related case: tasks that go to Rejected with No such image. Worker nodes pull the image themselves. For a private registry, deploy with docker stack deploy --with-registry-auth -c odoo-stack.yml erp so the credentials reach the workers.
Overlay DNS: Odoo cannot resolve "db"
Swarm's embedded DNS answers service names only for services on a shared overlay network. Test from inside the Odoo container, on the node where it runs (the official Odoo image has getent and the PostgreSQL client tools):
ODOO=$(docker ps -q -f name=erp_odoo | head -n1)
docker exec -it $ODOO getent hosts db # the service's virtual IP
docker exec -it $ODOO getent hosts tasks.db # the IP of each running task
- Nothing comes back: Odoo and PostgreSQL are not on the same network, or the database service has no running task. Compare the
networks:of both services. - The database lives in another stack: the short name only works inside one stack. Use the full service name (
otherstack_db) and attach Odoo to that stack's network, declared asexternal. dbresolves buttasks.dbis empty: the service exists but has no running task. Go back to Step 2 forerp_db.- Names resolve, connections hang: DNS is fine; the overlay data path is not. Check that 4789/udp and 7946/tcp+udp are open between nodes and that the overlay MTU fits the underlay. The Docker Swarm networking guide for Odoo covers both.
Odoo cannot reach PostgreSQL
Test the connection the same way Odoo makes it:
docker exec -it $ODOO pg_isready -h db -p 5432
docker exec -it $ODOO python3 -c "import urllib.request as u; print(u.urlopen('http://localhost:8069/web/health?db_server_status=1').read())"
The second command asks Odoo itself whether it can reach the database server (Odoo 17 and later). Then match the error in the Odoo log:
| Error in the Odoo log | Meaning |
|---|---|
could not translate host name "db" to address | DNS: see the previous section |
Connection refused | PostgreSQL is not running or not listening yet. Check erp_db tasks and logs |
| Connection timed out | Firewall between nodes, overlay MTU, or an external database host blocking the node's address |
password authentication failed for user "odoo" | Secret and database role are out of sync, often after a password rotation. See Odoo secrets on Swarm |
no pg_hba.conf entry for host | External PostgreSQL does not allow connections from the overlay or node address |
sorry, too many clients already | Connection pools add up past max_connections (see below) |
SSL connection has been closed unexpectedly on quiet instances | The server closes idle sessions (idle_session_timeout or a firewall idle timeout) and Odoo finds out on next use |
Too many clients. Every Odoo process keeps its own connection pool, up to db_maxconn (default 64). In prefork mode that is each HTTP worker, each cron worker and the websocket process, times every replica. Set db_maxconn in odoo.conf so that processes x db_maxconn x replicas stays below PostgreSQL's max_connections, with room for backups and admin sessions.
Idle disconnects. If your PostgreSQL server sets idle_session_timeout or idle_in_transaction_session_timeout, Odoo's long-lived background connections get closed while idle. You can switch both off for Odoo's connections only through the standard libpq variable, without touching the server:
environment:
PGOPTIONS: "-c idle_session_timeout=0 -c idle_in_transaction_session_timeout=0"
Healthchecks that restart a working Odoo
See the last probe results, with their output, for a running container:
docker inspect --format '{{json .State.Health}}' $ODOO
- Every probe fails with "executable file not found": the healthcheck calls
curlorwget, which the official Odoo image does not include. Use Python, which it does include: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 - Odoo is killed while starting or upgrading modules:
start_periodis too short for your database. Failures during that window do not count; raise it. - Every Odoo restarts when PostgreSQL blips: the healthcheck includes a database check (
db_server_status=1). Restarting Odoo does not fix PostgreSQL. Keep the healthcheck about Odoo's own HTTP server. - Restarts under heavy load: probes time out because all workers are busy. Add workers, or allow more
retriesso a 30-second spike does not trigger a restart, which only makes the load worse.
Websocket 400 errors and "real-time connection lost"
Since Odoo 16, live notifications, Discuss and chat use a websocket at /websocket. When it fails, users see stale screens and the browser console shows a failed WebSocket request with status 400. The Odoo log tells you which case you have:
Couldn't bind the websocket. Is the connection opened on the evented port (8072)?: Odoo runs withworkersabove 0, and the proxy sends/websocketto port 8069. In prefork mode, only the gevent process on port 8072 serves websockets. Add a separate route: in Traefik, a router withPathPrefix(`/websocket`)pointing at a service on port 8072.Invalid upgrade header,Invalid connection headerorEmpty or missing header(s): the proxy drops theUpgradeandConnectionheaders. Traefik passes them through by default. In nginx you needproxy_http_version 1.1plusproxy_set_header Upgrade $http_upgradeandproxy_set_header Connection "upgrade"on the websocket location.- Websocket works, but links and redirects use http:// or the wrong host:
proxy_modeis off, so Odoo ignores theX-Forwarded-*headers. Setproxy_mode = Truewhenever Odoo is behind a proxy.
A related 400: large uploads (database restores, big attachments) failing with "Bad Request: the browser (or proxy) sent a request that this server could not understand". Odoo's HTTP server gives each client socket a 2-second read timeout by default, and a pause in a slow upload ends the request. Raise it with the ODOO_HTTP_SOCKET_TIMEOUT environment variable (in seconds) on the Odoo service.
Updates that never finish or roll back
docker service inspect erp_odoo --format '{{json .UpdateStatus}}'
The state is updating, paused, completed, rollback_started, rollback_paused or rollback_completed, with a message. A rollback after every deploy usually means the new task never passed its healthcheck inside the update_config.monitor window: make monitor longer than start_period plus one healthcheck interval. An update stuck with the new task Pending is usually the host-mode port case above. The zero-downtime deployment guide covers safe update settings.
Node problems
- A node is Down:
docker node ps <node>shows what was running there. Check the Docker daemon on the node withsystemctl status dockerandjournalctl -u docker. - Disk full: image pulls and container starts fail with
no space left on device.docker system dfshows where the space went. Remove unused images withdocker image prune -a, but never prune volumes on a node that holds the database or filestore.
Troubleshooting checklist
docker node lsanddocker service ls: is it a node or a service?docker service ps --no-trunc: read the ERROR column and the task history.docker service logs: read the last lines before each exit.- Stop the churn (rollback or scale to 0) before changing anything else.
- From inside the Odoo container:
getent hosts db, thenpg_isready. - Check healthcheck results with
docker inspect. - Change one thing at a time, and write down what you changed.
Most of these problems are easier to catch before users report them. The guide to monitoring Odoo on Swarm sets up alerts for task churn, killed requests and failing scheduled actions.
Easy Instance runs Odoo on Docker Swarm and has already been through these failures, so its instances ship with a Python healthcheck, a separate websocket route, long stop grace periods and connection settings that survive idle timeouts. If you would rather not debug a cluster, see pricing or start a 10-day free trial, no credit card needed.