Skip to main content
Separate deployments on one host — one per customer, or one per environment. Not a cluster: these are independent gateways with their own databases and their own traffic. For why several instances against one database does not work, see Clustering.

One name, two products

--tenant is the whole idea: the same word names the same instance to both installers. One name per customer, two products.

On update it is a lookup, not a layout

This is the part that prevents the expensive mistake.
On update, service and rollback, --tenant finds the install with that name and uses its own paths, unit, account and ports.
Without that, an upgrade aimed by forgetting a flag would stop a unit that may not exist, replace a directory belonging to another instance, and enable a second service against the same database.A unit whose WorkingDirectory= names a different install is refused, before anything is stopped.

Ports

Give a band and the installer takes the next free listener port, so instance two does not collide with instance one. --http-port and --ops-port are set per instance too. Both installers read the ports back out of the environment file on update, so an instance on non-default ports stops failing its own verify at the end of every upgrade.

A virtualhost

Puts the instance under a per-customer home: /home/websmsc/gateway, with the panel at /home/websmsc/panel. What it buys: both generated units mount a tmpfs over /home and bind only that one virtualhost back in, so a gateway cannot see any other customer’s home directory.
--user is required here, and it is the step most easily missed. A home directory is 0750 and owned by its own account, and the installer will not chown a directory that already exists — that guard is deliberate, so an install can never rewrite the ownership of everything already in someone’s home.So a service running as any other account simply cannot traverse the virtualhost. ProtectHome=tmpfs and BindPaths= make the directory visible inside the unit’s namespace; neither changes a permission bit.The symptom does not name the cause: the panel dies at npm ci with can't cd to /home/<name>/panel/app, and the gateway registers a unit that then cannot read its own configuration. Pass the owning account to both installers.
Under --vhost, the panel’s CDR_EXPORT_DIR must also sit inside a BindPaths= root. That failure is silent — exports are prepared and then cannot be read.
--vhost alone, without --tenant, means something different — do not use it for a new tenant. It keeps an older flat layout, PATH/{app,conf,data}, instead of PATH/panel.That compatibility exists so panels installed under /home/<name> before --tenant are reached exactly as before, and an upgrade never relocates a running install. On a new tenant it silently puts the gateway and the panel in different layouts — the gateway under /srv/fireflo/<name>/gateway and the panel loose in /home/<name> — after which update --tenant <name> no longer finds the panel. Give both flags together.
The three directory flags — --app-dir, --conf-dir, --data-dir — still win over either scheme wherever they are given.

What each instance needs of its own

Removing a tenant

There is no uninstall verb. Both installers offer check, install, update, service and rollback — removal is manual, which is why the units are worth enumerating rather than guessing at.
A tenant is four systemd units, not two. The gateway writes three: The logclean timer is the one that gets left behind. Remove only the main unit and it stays enabled, firing every day against a log directory that no longer exists. Look before removing — this changes nothing:
Then the units, all four:
Then the directories — whichever layout this tenant used:
Loaded: not-found alongside Active: failed is normal here, not a fault. It is systemd’s runtime memory of a unit whose file has gone; reset-failed clears it. Equally, a status=143 exit in the journal is 128 + SIGTERM — a clean stop, not a crash.

What this deliberately leaves alone

The database. A tenant’s routing, vendors, listeners, logins, call records and balances live there, not in the install directory — which is what makes a reinstall safe, and what makes dropping the database a separate decision:
The service account, which may be shared with other services or be a real customer login. Remove it by hand only once you are sure nothing else uses it. Firewall rules, if the install used --firewall. Check sudo ufw status numbered for this tenant’s SMPP range and HTTP port.

Reinstalling rather than removing

Relocating an instance is not a mv. fireflo.env records its own absolute paths, and only some are rewritten by later verbs — FIREFLO_CONF_DIR, FIREFLO_DATA_DIR and QUARKUS_LOG_FILE_PATH are written by install alone. A moved install keeps writing its call records and logs to the old location while the new unit’s ReadWritePaths= covers only the new one. Remove and reinstall instead. The database survives, so the fresh install omits --bootstrap — it refuses on a database that already has a routing table, a worker or a login.