Skip to content

Multiple instances

Omisen edited this page Aug 17, 2026 · 3 revisions

More than one Odoo on the same machine: how an instance is created, what each one owns, what they all share, and what happens when you remove one.

(For what a rollback does with the shared half: The rollback model.)


An instance is created by naming it

sudo invok                                     # the form asks for the name, first
sudo invok --instance cliente-x --port 8169    # a second one, beside it

Re-running the installer does not add an instance. Without a name it targets the installation already registered on the machine and stops rather than overwrite it — the message names the three ways on, and adding a second instance is the first of them.

There are now three places a name can come from, in this order: --instance on the command line, the interactive form, and ODOO_INSTANCE in a .env. The form asks:

? Instance name (empty = the historical instance)
  a second Odoo beside an existing one needs a name; leave empty for the first

Leaving it empty is a real answer — it means the historical instance, and it does not fall back to the .env; whoever is at the terminal has the last word. An instance is still an explicit decision: you have to type a name for one to exist.

It is the first question, and that is the feature rather than a matter of tidiness. The form always fills in the user, the database and the port, even when the answer is the suggested default, and those answers outrank the names the installer would otherwise derive from the instance. Asked at the end — where the flow would naturally put it — a named second instance would have taken the first one's user, database and port: a collision on all three, produced by the very question meant to make a second instance easy. Asked first, the suggestions below it move with the name: odoo-cliente-x for the user and the database, /opt/odoo/odoo-cliente-x for the sources.

The port is not proposed, and that is deliberate: a free one cannot be known before the manifests are read, which needs root, which would have to happen before the form. If the port collides, the preflight refuses — reading it from the other manifests rather than probing the socket — and names the other instance and which of the two ports clashed.

The name is validated against the intersection of the five grammars it ends up in — a systemd unit, a path, a PostgreSQL identifier, an nginx server_name, a Unix user:

^[a-z][a-z0-9_-]{0,25}$

Lowercase only, because two of those five fold case and one does not — allowing both would let Cliente and cliente name the same role and two different directories. The 26-character cap is not a round number: the system user is odoo-<name> and a Unix user name stops at 32.

default is reserved: it is how the unnamed instance is named when one has to be typed, to rollback --instance and in invok list. An instance actually called default would make a destructive command's selector ambiguous, and ambiguity there is not something to settle with a precedence rule.

What each instance owns

Everything, under its own name:

Unnamed instance --instance cliente-x
directory /opt/odoo/odoo18 /opt/odoo/odoo-cliente-x
system user odoo odoo-cliente-x
PostgreSQL role odoo odoo-cliente-x
database odoo odoo-cliente-x
service unit odoo18.service odoo-cliente-x.service
config file odoo18.conf odoo-cliente-x.conf
helper command odoo odoo-cliente-x
filestore /opt/odoo/.local/share/Odoo /opt/odoo/odoo-cliente-x/.local/share/Odoo
manifest /var/lib/invok/state.json /var/lib/invok/instances/cliente-x.json

Two things follow, and they are the point of the design.

The unnamed instance is unchanged, byte for byte. Whoever does not ask for an instance must not notice that instances exist: every path, unit and user keeps the name it has had since the first release, so an installation made a year ago is still found, still resumable and still removable.

The role has the same name as the user, deliberately. Odoo connects over the local Unix socket, where pg_hba.conf usually says peer — which authenticates by operating-system user and ignores the password. A role named differently would simply be refused. That is also why each instance gets its own system user: it is what makes its own database reachable and nobody else's.

Ports

Each instance takes two: the HTTP one, and the longpolling port Odoo's gevent worker binds when it runs with more than one worker.

sudo invok --instance cliente-x --port 8169    # HTTP 8169, longpolling 8172

--port moves the pair — the second is derived at +3, which reproduces the familiar 8069 → 8072 — so a second instance normally needs nothing else. --gevent-port is there for a machine that already uses that number.

A port another instance has recorded is refused before anything is touched, even when nothing is listening on it:

Error: instance 'default' already claims port 8072, even if nothing is listening on it right now.

That is not pedantry. With the default single-worker configuration the longpolling port is never bound, so the system would answer "free" — and the collision would surface at the first customer who raises workers, in production rather than in testing.

What they share

Some things exist once per machine by construction:

  • /opt/odoo, the root every instance lives under;
  • the system packages and the build dependencies;
  • the PostgreSQL cluster;
  • wkhtmltopdf;
  • the nginx installation and its firewall rule.

They belong to whichever instance created them, and that ownership is recorded. A second instance finds them already there, touches none of them, and adds no package at all.

One consequence you may see on disk: if the first instance owns /opt/odoo as 0750, a named instance's user cannot walk through it to reach its own home. The installer widens it by exactly one bit — o+x: walk through, still not list — records that it did, and puts it back when no named instance needs it any more. When it keeps the bit because somebody still needs it, the rollback says so, naming the mode it is holding and who needs it; before 3.2.0 it kept it silently, and a changed permission with nothing to attribute it to is worse than the permission itself.

There is one case where the bit outlives everybody, and it is declared rather than fixed: with two named instances, if the one that widened is removed first, the second has no mode to put back — it found the root already traversable. invok rollback --all still restores it, because the widener's manifest is kept as the record. Removing named instances one at a time on a machine that keeps the historical one leaves o+x behind: one traversal bit on a directory whose contents stay 0750, which exposes nothing — you cannot list it, and you cannot read what you had no right to.

Seeing what is installed

sudo invok list
INSTANCE             ODOO    STATE         DATABASE               DIRECTORY
default              18.0    installed     citest                 /opt/odoo/odoo18
cliente-x            18.0    installed     odoo-cliente-x         /opt/odoo/odoo-cliente-x

  default              /var/lib/invok/state.json
  cliente-x            /var/lib/invok/instances/cliente-x.json

It needs root, because the manifests are 0600 root: a listing that silently came up empty for lack of privileges would be worse than a refusal. For the same reason a manifest that exists but cannot be read is reported as a problem rather than skipped.

Removing one, or all

sudo invok rollback --instance cliente-x   # that one
sudo invok rollback --all                  # every one, in the right order

With more than one installed, rollback on its own lists them and stops: it does not guess when the choice is destructive. What happens to the shared half — left in place, reported, and recorded in a manifest kept as a tombstone — is in The rollback model.

Limits worth knowing

  • One PostgreSQL cluster. Instances have separate roles and databases inside it, not separate clusters. Stopping PostgreSQL stops all of them.
  • Same-version instances are fine. Two Odoo 18 side by side collide over nothing: since the name qualifies every artifact, the version stopped being what tells installations apart.
  • The historical instance keeps its filestore where it has always been (/opt/odoo/.local/share/ Odoo), and that is deliberate: moving it would make an existing installation start against an empty directory, with no error and no attachments.
  • --with-nginx on more than one instance gives each its own vhost, upstreams and log files. They must differ in --server-name, or the two vhosts answer for the same hostname and nginx serves whichever it loaded first.

Clone this wiki locally