Odoo Docker Swarm: How Easy Instance Runs Odoo

How Easy Instance runs Odoo on Docker Swarm: one stack per instance, Traefik routing, region placement, a shared filestore and scale-to-zero.

Updated . 8 min read

Easy Instance hosts every customer's Odoo on Docker Swarm. This guide walks through that Odoo Docker Swarm setup: what each instance looks like, how traffic reaches it, how deploys avoid downtime, where files live, and what happens when an instance is stopped. You can reuse the same patterns to run Odoo for several companies or teams on your own cluster.

Why Docker Swarm for Odoo

An Odoo instance is a simple workload: one Odoo service, one PostgreSQL database and a filestore. Swarm fits that shape well:

  • It ships with Docker Engine. There is no separate control plane to install or upgrade.
  • Stack files use the same YAML format as Docker Compose, so one generated file describes one instance.
  • It has the pieces a multi-tenant Odoo platform needs: placement constraints, resource limits and reservations, health-checked rolling updates with automatic rollback, encrypted overlay networks and secrets.

Swarm doesn't autoscale services and has a smaller storage-plugin ecosystem than Kubernetes. Both gaps are covered below. If you are still choosing a runtime, read Odoo on Docker Compose vs Swarm first.

The Odoo SaaS architecture at a glance

  • Manager nodes hold the Swarm state and run the control panel, which is itself an Odoo application. It writes each instance's stack file and applies it with docker stack deploy.
  • Worker nodes run the customer Odoo containers. One pool is shared and grows or shrinks with demand. Regions and dedicated servers get their own labeled workers.
  • PostgreSQL runs on a dedicated database server outside the stacks. Instance stacks contain no database container.
  • Traefik is the single entry point. It reads routing rules from service labels, terminates TLS and forwards requests over an overlay network.
  • Shared storage holds each instance's filestore, config and custom addons, so a container can start on any eligible node.

For how managers, workers and Raft fit together, see Docker Swarm architecture.

One stack per Odoo instance

Each customer instance is its own stack with one service. Keeping instances in separate stacks means you can deploy, restart, scale or remove one without touching any other. A simplified version of what gets generated looks like this:

odoo-instance.yml (simplified)
services:
  odoo:
    image: odoo:19.0
    networks:
      - web
    stop_grace_period: 300s
    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
    volumes:
      - /shared/instances/shop1/data:/var/lib/odoo
      - /shared/instances/shop1/config:/etc/odoo
      - /shared/instances/shop1/addons:/mnt/extra-addons
    deploy:
      placement:
        constraints:
          - node.role == worker
          - node.labels.region == shared
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 5
      update_config:
        order: start-first
        failure_action: rollback
        monitor: 180s
      rollback_config:
        order: start-first
      resources:
        limits:
          cpus: "2.0"
          memory: 3g
        reservations:
          cpus: "0.5"
          memory: 1g

networks:
  web:
    external: true

A few choices in that file came straight out of production incidents:

  • The health check uses Python, not curl. The official Odoo image always has python3, so the check never depends on an extra package. It calls /web/health, which catches an Odoo server whose workers are hung even though the process is still alive.
  • A long stop_grace_period. Docker's default is 10 seconds. A large module install can still be writing to the database when Swarm sends SIGTERM. A SIGKILL at that point leaves modules half-installed. Five minutes gives heavy installs time to finish.
  • Retries and start_period are generous. A busy Odoo instance during a load spike should not be restarted as if it had crashed.

Routing Odoo traffic with Traefik labels

Traefik v3 watches the Swarm API through its swarm provider and builds routes from labels under deploy.labels. Every instance gets two routers: one for normal HTTP traffic and one for Odoo's websocket. When Odoo runs with workers (prefork mode), /websocket must go to the gevent port 8072. In threaded mode (workers = 0) everything stays on 8069.

Traefik labels for one instance
    deploy:
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.shop1.rule=Host(`shop1.example.com`)"
        - "traefik.http.routers.shop1.entrypoints=websecure"
        - "traefik.http.routers.shop1.tls.certresolver=le"
        - "traefik.http.routers.shop1.service=shop1"
        - "traefik.http.services.shop1.loadbalancer.server.port=8069"
        - "traefik.http.routers.shop1-ws.rule=Host(`shop1.example.com`) && PathPrefix(`/websocket`)"
        - "traefik.http.routers.shop1-ws.entrypoints=websecure"
        - "traefik.http.routers.shop1-ws.tls.certresolver=le"
        - "traefik.http.routers.shop1-ws.service=shop1-ws"
        - "traefik.http.services.shop1-ws.loadbalancer.server.port=8072"

Custom domains are just more Host() rules on the same routers. Set proxy_mode = True in odoo.conf so Odoo trusts the forwarded headers. For load balancing several Odoo containers behind one hostname, see Odoo load balancing.

Zero-downtime deploys: start-first plus health checks

Swarm's default update order is stop-first. With one replica, that leaves a moment where the service has no running task. Traefik only builds routes from running tasks, so its router disappears and visitors get a 404 rather than a short delay. Easy Instance uses order: start-first instead. The new container starts and has to pass its health check before the old one stops.

  • monitor: 180s is longer than start_period plus one check interval. A 30-second monitor would mark the update as done before the new task could prove it was healthy.
  • failure_action: rollback brings back the previous version automatically. rollback_config uses the same order, so a rollback doesn't create the gap the update just avoided.
  • One exception: if a service publishes a host-mode port, start-first cannot work. Two tasks cannot bind the same port on one node, so the new task sits in Pending forever. Those services keep stop-first.

The full set of update options is covered in Odoo zero downtime deployment.

Restart tip: changing a service label (under deploy.labels) updates the service but does not replace its tasks, because it is outside the task template. To force a fresh container, change something in the task template, such as a container label, or run docker service update --force.

Placing instances by region with node labels

Every worker node carries a region label. An instance in a specific region gets the constraint node.labels.region == <region>. Everything else gets node.labels.region == shared, which pins it to the shared, autoscaled pool. Use a positive match like this rather than a list of regions to avoid. An exclusion list is frozen into each service when it is deployed. When a new region was added later, older services that didn't exclude it were free to move there, and Swarm moved them. More on constraints and preferences: Odoo server placement in Swarm.

A shared filestore so containers can move

Swarm's local volumes stay on one node. If an Odoo task is rescheduled after a node failure, a local filestore would be left behind, and attachments and images would break. Easy Instance bind-mounts each instance's data directory, config and addons from storage that every eligible node mounts. That covers the filestore and sessions in Odoo's data_dir, plus odoo.conf. Databases stay on the PostgreSQL server, not on shared storage. NFS has its own quirks, such as "silly-renamed" .nfsXXXX files that make a delete fail while a process still holds the file open. Setup, mount options and alternatives are in Odoo filestore on NFS.

Packing instances by reservations, not usage

Swarm places tasks based on their reservations, not on what they actually use. A small CPU reservation lets many quiet Odoo instances share a node, while the CPU limit still caps how far each one can burst. Memory works differently. RAM can't be safely overcommitted, because a node that runs out kills processes. So memory reservations are kept close to real usage. The node autoscaler sizes the shared pool against the same reservation numbers, which keeps the scheduler and the capacity planner in agreement.

Customers scale by adding Odoo workers from the dashboard. How workers, replicas and nodes relate is in Scale Odoo: workers vs replicas.

Scale-to-zero for stopped and suspended instances

Stopping an instance, whether the customer clicks Stop, a trial expires or a subscription lapses, scales its service to 0 replicas instead of removing the stack:

  • It is reversible. If a later step fails, the service still exists and can be scaled back up.
  • The stack file, labels and constraints stay in place, so starting again is a single scale command.
  • Monitoring sees desired = 0 and reads it as an intentional stop. A missing service would read as an outage.

Traefik drops the router of a service that has no running tasks. So each instance also has a low-priority fallback route that is only used when the real one is gone. A stopped instance then shows a "stopped" page instead of a generic error. An idle trial that has been put to sleep shows a wake page: visiting it starts the instance again.

What Swarm does not do for you

  • No service autoscaler. Scale nodes with your cloud provider's API and scale Odoo by workers or replicas yourself.
  • Limited storage plugins. CSI cluster volumes exist, but few plugins are packaged for Swarm. NFS is still the practical default.
  • Your own observability. Metrics and logs need a separate stack. See monitor Odoo on Swarm.

Go deeper

Skip running the cluster yourself

All of this runs behind the Easy Instance dashboard. You pick an Odoo version and get an instance with TLS, daily backups, staging and GitHub deploys. Pricing is per Odoo worker, not per user (see pricing). You can try it free for 10 days with no credit card.

Rather not run the server yourself?

Get managed Odoo hosting with backups, SSL, monitoring and updates handled for you. Free for 10 days.