> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fonoster.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Install with Docker Compose

> Run Fonoster on one machine with Docker Compose, from download to signed in to the dashboard.

![One machine, every service: a terminal shows docker compose ps with all eleven Fonoster services up.](https://mintcdn.com/fonoster/GsxtawNPYOQvohG4/images/install-compose/banner-light.png?fit=max\&auto=format\&n=GsxtawNPYOQvohG4\&q=85\&s=5677c5771ac0af698f6af2e0bfb761b7)

In this tutorial you install Fonoster on one machine. At the end, every service is running and you
are signed in to the dashboard as the owner, ready to make your first call.

## Before you begin

You need:

* [Docker](https://docs.docker.com/get-docker/) with Compose v2 (`docker compose version` prints
  `v2` or later).
* [Node.js 22](https://nodejs.org/en/download/) or later. You use it to run the setup tool with `npx`.
* `curl`.
* About 6 GB of free disk for the images.

> [!NOTE]
>
> Fonoster uses port `8449` (dashboard and API), `5060` UDP and TCP (SIP), the UDP ranges
> `10000-10999` and `11000-11999` (call media), and, on localhost only, `4222` (NATS) and `9000-9001`
> (MinIO). Stop anything else that uses them, such as another Fonoster install, or change the ports
> in step 2.

> [!NOTE]
>
> If this machine ran Fonoster 0.27 or older with Docker Compose, stop it first: run
> `docker compose down` in its folder. Don't remove its volumes. The new install doesn't use them.

## Install Fonoster

**1. Download the release**

Download the release and unpack it into a new `fonoster` folder:

```bash theme={"system"}
curl -fsSL https://github.com/fonoster/fonoster/releases/download/v0.30.0/fonoster-0.30.0.tar.gz | tar xz
cd fonoster
```

The folder holds `compose.yaml`, `compose.tls.yaml` (for HTTPS, below), `.env.example`, a
`config` folder with one example per service and their schemas, and a `README.md`.

**2. Generate the configuration**

Run `init` to generate the secrets and keys, find this machine's address, and write the
configuration:

```bash theme={"system"}
npx -p @fonoster/common fonoster-config init
```

It prints what it wrote:

```text theme={"system"}
.env written (10 secrets generated)
keys: 3 files created in config/keys
config: config/autopilot.yaml, config/fonoster.yaml, config/appbridge.yaml, config/roomr.yaml, config/routr-edgeport.yaml
host address: 192.0.2.10

Next: docker compose up -d
Then: npx -p @fonoster/common fonoster-config info (dashboard URL and owner login)
```

The host address is the one softphones, carriers and browsers use to reach this machine. To use a
different one, run `init` again with `--host-ip <address>`.

> [!TIP]
>
> To change a port, edit it in `.env` (for example `HTTP_PORT=8449` or `SIP_PORT=5060`) and run
> `init` again, then `docker compose up -d`. `init` keeps every secret and only updates the
> configuration. To run a second install on the same machine, also give it its own
> `COMPOSE_PROJECT_NAME` and `SUBNET_PREFIX`, and move every port in the note above.

**3. Check the configuration**

Validate the configuration before you start:

```bash theme={"system"}
npx -p @fonoster/common fonoster-config check
```

Each service reports `valid`:

```text theme={"system"}
config/autopilot.yaml: valid
config/fonoster.yaml: valid
config/appbridge.yaml: valid
config/roomr.yaml: valid (no schema; values only)
config/routr-edgeport.yaml: valid (no schema; values only)
```

**4. Start Fonoster**

Start every service in the background:

```bash theme={"system"}
docker compose up -d
```

The first start downloads the images, which takes a few minutes. Then list the services to see
them running:

```bash theme={"system"}
docker compose ps
```

You see `apiserver`, `autopilot`, `dashboard`, `envoy`, `appbridge`, `roomr`, `minio`, `nats`,
`postgres`, `routr` and `rtpengine`, each `Up`.

**5. Get your login**

Print the dashboard address and the owner's login:

```bash theme={"system"}
npx -p @fonoster/common fonoster-config info
```

```text theme={"system"}
Dashboard   http://192.0.2.10:8449
Owner       admin@fonoster.local / <generated password>
API         192.0.2.10:8449 (gRPC and gRPC-Web, plain HTTP)
SIP         192.0.2.10:5060 (UDP and TCP)

Files       /home/you/fonoster/.env   Compose settings and the generated secrets (back it up)
            /home/you/fonoster/config/   service settings (from config/*.example.*)
            /home/you/fonoster/config/keys/   signing keys
```

**6. Sign in to the dashboard**

Open the dashboard address in your browser and sign in with the owner's email and password.
On **Choose a workspace**, select your workspace.

> [!TIP]
>
> Fonoster is running and you are signed in as the owner.

## Where your files are

Everything lives in the `fonoster` folder:

| Path | What it holds |
| - | - |
| `.env` | Ports, the host address, the owner's login and every generated secret |
| `config/` | Each service's settings, written by `init` from the examples. They name secrets as `${NAME}`; Compose passes each container only the values its own file names (its `environment:` list in `compose.yaml`) |
| `config/keys/` | The signing keys. Only the API server mounts Identity's private key; AppBridge gets the public key and its call-record key |

> [!WARNING]
>
> Back up `.env`. Its encryption key protects the vendor keys you store, and they can't be recovered
> without it.

## Add platform keys

Calls need at least a text-to-speech vendor. Fonoster offers every vendor in its catalog to every
workspace, and calls use your platform key for that vendor unless the workspace added its own in the
dashboard under **Vendor keys**.

`init` writes a placeholder platform key for each vendor. Replace them with your keys:

| Vendor | File | Setting |
| - | - | - |
| Deepgram | `config/appbridge.yaml` | `providers.deepgram.credential.value` |
| ElevenLabs | `config/appbridge.yaml` | `providers.elevenlabs.credential.value` |
| OpenAI | `config/autopilot.yaml` | `credentials.openai-default.value` |
| Groq | `config/autopilot.yaml` | `credentials.groq-default.value` |

Then restart the two services that read them:

```bash theme={"system"}
docker compose restart appbridge autopilot
```

> [!NOTE]
>
> A vendor whose placeholder you leave in place is still offered, and its calls fail until a key is
> set. If the dashboard's **Vendor keys** page says no vendors are configured, the catalog has no
> vendor left that takes a key.

## Serve over HTTPS

By default the dashboard and API use plain HTTP on port `8449`, on every interface. That's fine on
a trusted network. On the internet, passwords, API keys and tokens would cross it in clear text, so
`init` and `check` print a warning when `PUBLIC_URL` is `http://` on an address that isn't
loopback or private. Turn on HTTPS in one of two ways.

### Envoy with your certificate

You need a domain name that points to this machine, and a certificate and private key for it in
PEM files.

**1. Get a certificate**

With [Let's Encrypt](https://letsencrypt.org/), get one with `certbot` while ports `80` and
`443` are still free:

```bash theme={"system"}
sudo certbot certonly --standalone -d voice.example.com
```

It writes `fullchain.pem` and `privkey.pem` in `/etc/letsencrypt/live/voice.example.com/`.
Any CA works: use your own files instead.

**2. Turn on HTTPS in .env**

Uncomment and fill these lines in `.env`, and set `PUBLIC_URL` to the certificate's domain:

```bash theme={"system"}
COMPOSE_FILE=compose.yaml:compose.tls.yaml
PUBLIC_URL=https://voice.example.com
TLS_CERT=/etc/letsencrypt/live/voice.example.com/fullchain.pem
TLS_KEY=/etc/letsencrypt/live/voice.example.com/privkey.pem
```

`COMPOSE_FILE` adds `compose.tls.yaml`, which mounts the two files read-only into Envoy.

**3. Apply it**

Write the configuration again and recreate the services:

```bash theme={"system"}
npx -p @fonoster/common fonoster-config init
docker compose up -d
```

`init` warns if a file is missing or `PUBLIC_URL` isn't `https://`. Envoy now serves HTTPS on
port `443` and redirects port `80` to it. Port `8449` is no longer published. Browser calls
switch to `wss://voice.example.com/sip` on their own.

Envoy reads the certificate when its container is created. After a renewal, recreate it. Because
Envoy holds port `80`, stop it while `certbot` renews:

```bash theme={"system"}
sudo certbot renew \
  --pre-hook "cd /path/to/fonoster && docker compose stop envoy" \
  --post-hook "cd /path/to/fonoster && docker compose up -d --force-recreate envoy"
```

### A reverse proxy in front

If you already run a proxy that terminates TLS (Caddy, nginx, Traefik, a cloud load balancer),
keep Envoy on plain HTTP and publish it on this machine only:

```bash theme={"system"}
HTTP_BIND=127.0.0.1
PUBLIC_URL=https://voice.example.com
```

Run `init` and `docker compose up -d`, then point the proxy at `127.0.0.1:8449`. The proxy must
forward gRPC (HTTP/2) and WebSocket upgrades on `/sip`.

### SIP over TLS

SIP stays on UDP and TCP `5060`. To also accept SIP over TLS on `5061`, convert your certificate
into a PKCS#12 keystore for Routr:

```bash theme={"system"}
mkdir -p config/tls
openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem \
  -out config/tls/signaling.p12 -passout pass:<password>
```

Then uncomment the `routr` lines in `compose.tls.yaml` and the `tls` transport in
`config/routr-edgeport.example.yaml`, set its `keyStorePassword` and `trustStorePassword` to
`<password>`, and run `init` and `docker compose up -d`. Carriers and softphones then use
`transport=tls` on port `5061`. Call media (RTP) isn't encrypted by this.

## Let carriers and phones reach you

Fonoster names SIP addresses after your `domain` in `config/fonoster.yaml`, the way Twilio does:

* **Trunks**: a trunk's inbound URI, where your carrier sends calls, is one label under
  `pstn.<domain>`. A trunk named Acme gets `acme.pstn.example.com`.
* **Domains**: a domain, where SIP agents register, is one label under `sip.<domain>`, such as
  `acme.sip.example.com`.

The API rejects any other name. Point two wildcard DNS records at your SIP address, so every new
trunk and domain resolves without a new record:

```text theme={"system"}
*.pstn.example.com  →  your SIP address
*.sip.example.com   →  your SIP address
```

If your carriers and phones reach you by IP address, or you can't add wildcard records, accept any
host or IP instead. Set this in `config/fonoster.yaml`, then restart the API server:

```yaml theme={"system"}
features:
  sip: { names: any }
```

The dashboard then asks for the full name instead of a label.

## Next steps

> **Make your first call**
>
> Add a vendor key, create an agent, and call it from a softphone.

> **Configure services**
>
> Change what each service does with the files in `config/`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.