Skip to main content
FireTone is three static binaries and a FreeSWITCH node. All are built with CGO_ENABLED=0, so they copy onto a machine and run.

Prerequisites

  • PostgreSQL 14 or later
  • Redis, on loopback
  • FreeSWITCH, on the node
  • ffmpeg, for media conversion

Bringing it up

1

Database

Create the role and database, then set FIRETONE_DATABASE_URL.
2

Build

3

Migrate

Migrations are compiled into firetonectl. If you change one, rebuild the binary or it will keep applying the old text — including the error it printed last time.
4

Run

Health is on the API listener. Systemd units for both the daemon and the node agent are in deploy/.
5

The node

Build and configure FreeSWITCH from deploy/node/, then point it at the control plane’s ESL listener.

The daemon refuses to start if

These are deliberate refusals, not crashes:
  • FIRETONE_DATABASE_URL, FIRETONE_JWT_SECRET or FIRETONE_PROVISION_SECRET is missing.
  • The ESL, provisioning or media-stream listener is bound to 0.0.0.0. Those three serve SIP passwords and raw call audio; a wildcard bind is not a configuration choice it will accept.

Three that disable a feature rather than stopping the daemon

  • Without FIRETONE_SECRET_KEY, provider credentials cannot be stored and virtual agents fall back to an echo provider.
  • Without FIRETONE_PIN_PEPPER, agent PIN sign-in is disabled entirely rather than hashing without a pepper.
  • Without FIRETONE_FS_CONF_DIR, the daemon provisions no switch: a register-mode trunk never registers and the carriers ACL is never written, so adding an address under a trunk changes the database and nothing else. The trunk’s page says so; nothing else will.
The unit must also be able to write there. install-service.sh reads that variable and adds the generated directories to ReadWritePathsProtectSystem=strict makes everything else read-only whatever the ownership says, and the symptom is a “read-only file system” error that looks like a disk fault.

SIP-TLS for desk phones

A phone outside the office should register over TLS on 5061; on 5060 its password exchange and audio cross the internet in the clear. The switch config step turns TLS on by itself whenever it finds a certificate a phone could trust, and decides again on every run, so an update cannot switch it off:
  • sip-tls.pem in the secrets directory — key, certificate and chain in one file; or
  • a real certificate for the host installed as wss.pem (the self-signed one a proxied box generates for itself does not count).
The certificate must name the host phones are given — the node’s Handset server name — and that name must resolve to the node itself, not to a web proxy in front of the site. Where the site’s domain is behind Cloudflare, issue a separate certificate for an unproxied name such as sip.<domain> and install it as sip-tls.pem. SIP and media are IPv4 only. FT_SIP_TLS=1 insists and refuses to continue without one; FT_SIP_TLS=0 keeps it off. SIP_TLS_VERSION defaults to tlsv1.2,tlsv1.3. Every refusal happens before the running configuration is touched. Open 5061/tcp in the cloud security group; the host firewall rule is installed for you.
With TLS switched on and no certificate, FreeSWITCH does not refuse — it generates a self-signed certificate and listens anyway, which looks like success from outside. That is why the installer writes the certificate itself, and verify.sh checks that the one 5061 serves is the installed one.

Where the switch reaches the control plane

The switch connects to firetoned for every call (ESL) and every registration (the directory). Both addresses are read from firetoned’s own FIRETONE_ESL_LISTEN and FIRETONE_PROVISION_LISTEN when the switch config step runs, so the two ends agree. For a switch on another machine set FIRETONE_CONTROL_HOST to the control plane’s mesh address — ESL is plaintext, so never a public one.

Behind a reverse proxy

Set FIRETONE_TRUSTED_PROXIES. Without it every request appears to come from the proxy, and per-IP rate limits collapse onto one address — which means they protect nothing.

Two commands worth knowing

The environment file is 0600 and owned by the service user, so the obvious hand-typed forms fail in ways that blame the wrong thing. Both are shipped:
The staging installer (deploy/staging/install-ubuntu.sh) is run regularly and its gaps are fixed as they are found; a first run on a genuinely clean machine is still the less-travelled path. Expect to read the scripts as well as run them — each explains what it refuses to do and why.