Docker Swarm networking for Odoo decides whether your Odoo containers can reach PostgreSQL, whether Traefik can reach Odoo, and whether the Odoo websocket survives the trip through the cluster. Most problems come down to five things: overlay networks, the ports between nodes, the MTU, service discovery, and how ports are published. This guide covers each one, using an Odoo 19 and PostgreSQL 16 stack as the running example.
If you are new to running Odoo on Swarm, start with the pillar guide, Odoo on Docker Swarm, and come back here when you need to understand what happens between the nodes.
What an overlay network does
A bridge network only exists on one host. An overlay network spans every node in the swarm, so an Odoo task on node A can open a connection to a PostgreSQL task on node B as if both were on the same LAN. Docker builds this with VXLAN: each Ethernet frame from a container is wrapped in a UDP packet, sent across the normal host network (the "underlay"), and unwrapped on the destination node. Each overlay network gets its own 24-bit VXLAN ID, which keeps traffic from different networks apart.
Nodes only build the network locally when a task attached to it lands there. Until then, the network exists only in the swarm's cluster state.
Ports that must be open between nodes
Overlay networking fails silently when a firewall or cloud security group blocks one of these. Open them between all swarm nodes, and only between swarm nodes:
| Port | Protocol | Used for |
|---|---|---|
| 2377 | TCP | Cluster management (to managers) |
| 7946 | TCP and UDP | Node-to-node communication and network discovery |
| 4789 | UDP | Overlay (VXLAN) data traffic, including the ingress network |
The VXLAN port can be changed only when the swarm is created, with docker swarm init --data-path-port. If your nodes have a private and a public interface, use --advertise-addr and --data-path-addr on docker swarm init and docker swarm join so cluster and data traffic stay on the private network. Never expose 2377, 7946 or 4789 to the internet.
Designing overlay networks for an Odoo stack
A sound layout for Odoo uses two networks:
- proxy: shared by Traefik and every Odoo service that needs a public URL. Created once, outside any stack.
- backend: private to one stack, carrying only Odoo-to-PostgreSQL traffic. PostgreSQL is attached to this network and nothing else, so it is never reachable from the proxy or from other stacks.
Create the shared proxy network first:
docker network create --driver overlay --attachable proxy
--attachable lets standalone containers (for example a one-off debugging container) join the network. Without it, only swarm services can attach.
Then define the private network inside the 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:
- pgdata:/var/lib/postgresql/data
networks:
- backend
deploy:
placement:
constraints:
- node.labels.postgres == true
odoo:
image: odoo:19
command: ["odoo", "--proxy-mode", "--workers=2"]
environment:
HOST: db
USER: odoo
PASSWORD_FILE: /run/secrets/odoo_db_password
secrets:
- odoo_db_password
networks:
- backend
- proxy
deploy:
replicas: 1
labels:
- traefik.enable=true
- traefik.swarm.network=proxy
- traefik.http.routers.odoo.rule=Host(`erp.example.com`)
- traefik.http.routers.odoo.service=odoo
- traefik.http.services.odoo.loadbalancer.server.port=8069
- traefik.http.routers.odoo-ws.rule=Host(`erp.example.com`) && PathPrefix(`/websocket`)
- traefik.http.routers.odoo-ws.service=odoo-ws
- traefik.http.services.odoo-ws.loadbalancer.server.port=8072
networks:
backend:
driver: overlay
internal: true
proxy:
external: true
volumes:
pgdata:
secrets:
odoo_db_password:
external: trueA few details matter here:
internal: trueblocks outbound internet access from the backend network. Odoo still reaches the internet (mail servers, IAP, payment providers) through the proxy network. PostgreSQL does not need it.- The Traefik label
traefik.swarm.network=proxytells Traefik v3 which of Odoo's two networks to use. Without it, Traefik may pick the backend network, which it cannot reach, and you get 502 or 504 errors. - Traefik reads labels from
deploy.labelsin Swarm, and theloadbalancer.server.portlabel is mandatory there. Add your own entrypoint and TLS labels to both routers. - With
--workersabove 0, Odoo serves/websocketon the gevent port 8072, so it needs its own router. Sending it to 8069 makes Odoo reject the upgrade with a 400 error. - Inside the stack, Odoo reaches PostgreSQL by the short service name
db. Swarm's embedded DNS resolves it only for services on the same network.
Deploy it with docker stack deploy -c odoo-stack.yml erp. The docker stack deploy guide covers the stack workflow in detail.
Service discovery: VIP and DNS round robin
Every service on an overlay network gets a DNS name. By default (endpoint_mode: vip) the name resolves to one virtual IP, and Swarm's IPVS load balancer spreads connections across the service's tasks. Connections stay on the task they started on; new connections can land on another task.
The name tasks.<service> always returns the individual task IPs instead. That is what Prometheus DNS discovery and some debugging tools use.
Set endpoint_mode: dnsrr under deploy when you want the service name itself to return task IPs, for example when an external load balancer such as HAProxy does its own balancing. A dnsrr service cannot publish ports through the routing mesh.
For Odoo specifically: if you run more than one Odoo replica, the VIP balances TCP connections, not user sessions. Odoo keeps sessions in its filestore, so every replica needs the same filestore. Our guides on Odoo load balancing and shared filestore storage cover that setup.
The routing mesh and the ingress network
When a service publishes a port in the default ingress mode, Swarm opens that port on every node. A request to any node is forwarded over the special ingress overlay network to a healthy task, wherever it runs. That is how a single Traefik replica can receive traffic on ports 80 and 443 no matter which node your DNS points at.
The routing mesh hides the client's source IP from the container, because traffic is forwarded through other nodes. For a reverse proxy in front of Odoo that is usually a problem: Odoo's logs, login rate limits and IP-based rules all see an internal address. Publish the proxy's ports in host mode instead and run the proxy as a global service (or pin it to the nodes your DNS points at):
ports:
- target: 443
published: 443
protocol: tcp
mode: host
deploy:
mode: globalIn host mode the port opens only on nodes that run a task, there is no mesh forwarding, and two tasks of the same service cannot share a node. With update_config.order: start-first, that last point makes the replacement task wait in Pending forever, so keep the default stop-first order for host-mode services.
Odoo itself should not publish any ports. Traefik reaches it over the proxy overlay.
Customizing the ingress network
If the default ingress subnet overlaps a network your nodes already use (common with VPNs and some cloud VPCs), recreate it. Remove every service that publishes ports first, then:
docker network rm ingress
docker network create \
--driver overlay \
--ingress \
--subnet 10.11.0.0/16 \
--gateway 10.11.0.1 \
--opt com.docker.network.driver.mtu=1450 \
my-ingress
A swarm can only have one ingress network. To keep all overlay subnets clear of your existing ranges from the start, use docker swarm init --default-addr-pool. Address pools can only be set when the swarm is created.
Getting the MTU right
VXLAN adds 50 bytes of headers to every packet. On a standard 1500-byte underlay, Docker gives overlay interfaces an MTU of 1450 so encapsulated packets still fit. Docker does not detect a smaller underlay: it assumes 1500 unless you tell it otherwise. If your nodes connect over a link with a lower MTU (many cloud private networks and VPN tunnels such as WireGuard), you have to set it by hand.
The symptom is distinctive. Small requests work, but large ones hang: the Odoo login page loads while a big report, a file upload or a large PostgreSQL result set stalls until it times out.
The overlay driver's com.docker.network.driver.mtu option takes the underlay MTU, not the overlay MTU. Docker subtracts the 50 VXLAN bytes itself (and more on encrypted networks), so the container interfaces end up at underlay minus 50. Check the underlay with ip link show on each node, then pass that value when you create the network:
- Underlay 1500 (standard): leave the option out. Containers get 1450.
- Underlay 1450 (common on cloud private networks and tunnels): set 1450. Containers get 1400.
- Underlay 9000 (jumbo frames on a private LAN): set 9000 to cut per-packet overhead. Containers get 8950.
Setting the option to underlay minus 50 yourself subtracts the overhead twice. It still works, just with smaller packets than needed. Confirm the result with ip link show inside a container on that network.
docker network create \
--driver overlay \
--opt com.docker.network.driver.mtu=1450 \
backendIn a stack file, set the same option under driver_opts on the network. Every node in the path must support the underlay MTU; one link with a smaller MTU breaks large packets for the whole network.
Encrypting overlay traffic
Swarm encrypts its own control traffic with mutual TLS, but application traffic on an overlay is not encrypted by default. Odoo-to-PostgreSQL traffic carries passwords and business data, so if your nodes talk over a network you do not fully control, encrypt it:
docker network create --driver overlay --opt encrypted backend
This turns on IPsec at the VXLAN level. It has a real CPU and throughput cost, so test it under load. Firewalls must also allow IPsec ESP traffic (IP protocol 50) between nodes, and encrypted overlays are not supported for Windows containers. The ingress network is not encrypted unless you recreate it with this option. An alternative is to leave the overlay unencrypted and turn on TLS in PostgreSQL itself.
Inspecting and checking overlay networks
# List overlay networks
docker network ls --filter driver=overlay
# Subnet, options, VXLAN ID and peers (run on a node that has a task attached)
docker network inspect erp_backend
# Which tasks of the stack run where
docker stack ps erp
To test DNS and connectivity from inside the Odoo container (the official image includes getent and the PostgreSQL client tools):
docker exec -it $(docker ps -q -f name=erp_odoo | head -n1) getent hosts db
docker exec -it $(docker ps -q -f name=erp_odoo | head -n1) pg_isready -h db -p 5432
Run these on the node where the Odoo task is running. If the name does not resolve, the two services do not share a network. If it resolves but pg_isready hangs, look at the firewall ports and the MTU above. The Odoo on Swarm troubleshooting guide walks through the full checklist.
Networking checklist for Odoo on Swarm
- Ports 2377/tcp, 7946/tcp+udp and 4789/udp open between nodes only.
- PostgreSQL on a private overlay, not on the proxy network, and never publishing 5432.
- Odoo on both networks, with no published ports, and Traefik told which network to use.
- A separate
/websocketroute to port 8072 when workers are above 0. - The overlay's MTU option set to the underlay MTU when the underlay is below 1500 (containers then get underlay minus 50).
- Encryption (or PostgreSQL TLS) whenever nodes talk over a network you do not control.
- Host-mode ports on the reverse proxy if Odoo needs real client IPs.
Easy Instance runs every Odoo instance on Docker Swarm with this kind of setup already in place: Traefik routing, the websocket route, healthchecks and backups. If you would rather run Odoo than run the cluster, see pricing ($30 per Odoo worker per month, no per-user fees) or start a 10-day free trial. No credit card needed.