Skip to main content
Several instances explains the mechanism — one --tenant word naming the same instance to both installers, and what --vhost buys. This page is the same information assembled into one run, for the moment you actually have a second customer to add. Nothing here is shared with the tenants already on the host except the packages, Java, Node and PostgreSQL themselves — see What each instance needs of its own. A new tenant gets its own database, its own ports, its own operational tokens and its own service account choice.
1

Pick the name and the layout

One word, used identically on both installers — it is what lets update find the right instance later without you re-typing paths.
Two layouts, pick one:Reach for --vhost when the tenant’s own SSH or SFTP account already lives under /home — it is what isolates the gateway from that account’s files, not just a different path. See A virtualhost.
Under --vhost, --user is not optional. A home directory is 0750 and owned by its own account, and the installer deliberately will not chown a directory that already exists — so a service running as any other account cannot traverse it.The failure is late and reads as something else: the panel dies at npm ci with can't cd to /home/<name>/panel/app, and the gateway starts and then cannot reach its own configuration. ProtectHome=tmpfs and BindPaths= make the directory visible inside the unit’s namespace; neither changes a permission bit.Pass the account that owns the virtualhost, on both installers.
--vhost without --tenant means something else. Alone, it keeps a legacy flat layout — PATH/{app,conf,data} — rather than PATH/panel. That form exists so panels installed before --tenant keep working, and for a new tenant it silently puts the gateway and the panel in different layouts. Always give both flags together.
2

Reserve ports

Every existing tenant on the host already holds an SMPP port, an HTTP port and an ops port. Pick a band the new one cannot collide with:
--ops-port is never opened in the firewall regardless of what you pass — it binds loopback only. --http-port is what a customer’s ESME or REST client actually reaches, so it is the one to check against ufw status before choosing it.
3

Its own database

One tenant, one database — never a schema shared with another tenant’s rows.
A separate role per tenant is not required — one fireflo role can own several databases — but it means a leaked credential for one tenant cannot read another’s. See The database.
4

Install the gateway

Under the virtualhost layout, add --vhost /home/$TENANT --user $TENANT — both, per the warnings in step 1.It asks first and shows you every decision before writing anything — add --yes only once you trust the answer it would print. --bootstrap writes the least a fresh database needs: a default routing table, an empty MESSAGE table, one SMPP listener, one login. Its generated password prints once; capture it.
Flags take space-separated values. --vhost=/home/$TENANT is rejected as an unknown option rather than parsed, on both installers.
--bootstrap only on a genuinely empty database. It refuses the moment it finds a routing table, a worker or a login — so reinstalling a gateway whose database already exists needs --config-source db on its own. Passing it the wrong way round is how an install ends up either refusing halfway or starting with nothing to route to.
Full flag list at fireflo-install.
5

Verify before adding anything

Exit code 4 here is expected and correct: the deployment is sound, it simply has nothing to route to yet. It becomes 0 once a vendor and a rule exist, in the next step’s follow-on — see First configuration.
6

Install the panel

Under the virtualhost layout, add --vhost /home/$TENANT --user $TENANT and point --gateway-conf-dir at /home/$TENANT/gateway/conf. Install the panel after the gateway — it reads that directory.
--port is never derived from --tenant. It stays 3000 unless given explicitly, so a second panel on the same host needs its own or the installer refuses rather than overwrite the first tenant’s unit.
Three things it works out for itself from --gateway-conf-dir: both operational tokens, the read-only METRICS_DATABASE_URL, and the ops URL — which it builds from that gateway’s recorded FIREFLO_OPS_PORT, so a non-default --ops-port needs no second flag here.
--config-url is never derived, on purpose — it is the connection that can change what a customer is billed. Without it the panel comes up read-only for this tenant.It is left off the command above deliberately: passed as a flag, the database password lands in your shell history and in ps output. Omit it and the installer prompts instead — and that prompt accepts the literal word same, meaning “reuse the connection just derived from the gateway”, which is the right answer whenever one role owns the tenant’s database.
7

TLS in front, one origin per tenant

The panel speaks plain HTTP on --port (default 3000, and it must be unique per tenant on a shared host — pass one explicitly for every tenant after the first). Put nginx or equivalent in front, terminating TLS at a hostname of this tenant’s own:
--public-url above must match server_name exactly — it is written as AUTH_URL, and without it a sign-out redirects to localhost regardless of what the proxy forwards. See TLS in front.
8

Finish the configuration

bootstrap deliberately leaves out a vendor and a rate — nobody else can guess a vendor’s credentials, and an invented price bills silently where an absent one shows up unpriced. Add both, then a routing rule naming the vendor, then this tenant’s first customer login — in that order, and why, at First configuration.
9

Send one, as a customer would

A 200 means accepted, charged and queued — watch the call record in this tenant’s panel for the delivery outcome.

What is genuinely new versus shared

Undoing this

There is no uninstall verb, and a tenant is four systemd units rather than two — the gateway’s -logclean.timer outlives a careless removal and keeps firing at a deleted log directory. The full procedure, and what it deliberately leaves alone, is at Removing a tenant.

Several instances on one host

The mechanism --tenant and --vhost implement, the update-is-a-lookup guarantee, and how to remove a tenant again.

First configuration, in order

What bootstrap writes, and the order that makes the rest of it meaningful.