Skip to content
MeridFlow AiFlow v8.x • self-hosted

Installation

AiFlow runs on your own infrastructure. This page takes you from an empty server to a working deployment, whichever way you prefer to get there, and then covers the commands you will use to run it day to day.

You do not need to read all of it. Pick a method from the table below, follow that one section, then jump to First steps.

What you are installing

Two containers and a database:

Service What it is Needs to be reachable
backend The API, the call engine, and the widget script it serves Yes, on its own hostname
admin The dashboard you configure agents in Yes, on its own hostname
Database SQLite by default, in a Docker volume. Postgres is optional No, internal only

Every installation method ends at the same place: those two containers behind HTTPS. Nothing is shared with other customers, and nothing phones home.

Before you start

You will need:

  • A server. 2 vCPU, 2 GB RAM and 30 GB disk is comfortable for a small deployment. Ubuntu or Debian is the easiest target. Images are built for both linux/amd64 and linux/arm64, so ARM instances and Apple Silicon both work.
  • Docker, with the Compose plugin. The installers below can put this on the server for you with --install-docker.
  • A Gemini API key. See Credentials.
  • Two subdomains of a domain you already control (see below).
  • A Twilio account, only if you want phone calls. The web widget alone needs nothing from Twilio. See Credentials.

You do not need a licence key to install. With none set, AiFlow runs in its free evaluation mode indefinitely: one agent, one conversation at a time, and the paid surfaces simply absent. Add a key later without reinstalling.

Domains

You need one domain you already control and two new subdomain records under it, one for the backend and one for the dashboard. You do not need to buy a domain, and your existing site is unaffected.

aiflow-api.example.com     A    <your server IP>
aiflow-admin.example.com   A    <your server IP>

They need separate hostnames because the dashboard is a static site and the backend is an API with WebSocket traffic; they cannot share one origin cleanly.

Point DNS before you install

Certificates are issued by proving you control the hostname. If DNS has not propagated yet, the TLS step fails and you will have to re-run it. Check with dig +short aiflow-api.example.com first.

Choosing a method

Method You need Good when
One-line installer A fresh server and one command Almost everyone. It runs one of the three below for you.
Docker Compose Docker, and a proxy you run You already have a reverse proxy, or want to see every step.
Coolify A VPS, no Docker knowledge You want a dashboard, not a terminal.
Your own reverse proxy Existing infrastructure Compliance rules, or an established proxy and TLS setup.

Trade-offs worth knowing before you pick:

  • The one-line installer is the least work and the least visibility. It is a wrapper around the other three, so anything it does you can also do by hand.
  • Docker Compose gives you the clearest mental model and the easiest debugging, at the cost of you arranging TLS. If you have no proxy yet, the installer's Caddy option sets one up for you.
  • Coolify gives you a web UI for environment variables, deploys and logs, and handles certificates. The trade-off is a second system to keep updated, and its first-run setup cannot be scripted.
  • Your own reverse proxy is the most control and the most responsibility. The one thing that catches people is WebSocket upgrade headers; see the checklist in that section.

One-line installer

The fastest path. It installs Docker if asked, fetches the right compose file, writes your .env, brings the stack up, and creates your first admin user.

curl -fsSL https://meridflow.com/downloads/install-aiflow.sh | sudo bash -s -- \
  --option caddy \
  --api-domain aiflow-api.example.com \
  --admin-domain aiflow-admin.example.com \
  --admin-email you@example.com \
  --install-docker

--option picks the topology and everything else is passed through to it:

--option What you get
caddy The stack plus a Caddy proxy that gets certificates automatically
compose The stack on plain HTTP, for you to put your own proxy in front of
coolify Prepares a Coolify install and prints the values to paste into it

Add --license-key AIFLOW1.... and --license-tier pro if you have a licence. Add --version 8.1.0 to pin an exact release.

See the flags for your topology

The flags differ by option, deliberately: a Caddy install needs domains, a plain Compose one does not.

curl -fsSL https://meridflow.com/downloads/install-aiflow.sh | bash -s -- --option caddy --help

And to see every .env variable AiFlow reads, with defaults:

curl -fsSL https://meridflow.com/downloads/install-aiflow.sh | bash -s -- --env-help

When it finishes, skip to First steps.

Docker Compose

What the installer does, by hand.

1. Fetch the compose file and environment template

mkdir aiflow && cd aiflow
curl -O https://raw.githubusercontent.com/maelqo/scripts/main/aiflow/config/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/maelqo/scripts/main/aiflow/config/.env.example
mv .env.example .env

2. Fill in .env

At minimum:

SECRET_KEY=          # any random 32+ byte string: openssl rand -hex 32
GEMINI_API_KEY=      # from Credentials
PUBLIC_BASE_URL=https://aiflow-api.example.com

PUBLIC_BASE_URL must be the backend's real public HTTPS URL. It is what Twilio calls back on and what the dashboard is pointed at, so a wrong value here is the single most common cause of a deployment that starts but does not work.

Add a licence with AIFLOW_LICENSE_KEY= and AIFLOW_MODE=live. Leave both alone to stay on the free evaluation tier.

3. Pull and start

Two images, published to GitHub Container Registry on every release:

ghcr.io/maelqo/aiflow-backend
ghcr.io/maelqo/aiflow-admin

They are public. There is no registry login, no GitHub account, and no token, so you can fetch either one directly to check connectivity or to pre-pull before a maintenance window:

docker pull ghcr.io/maelqo/aiflow-backend:8

What a running deployment is allowed to do is decided by its licence key at runtime, not by whether the image was hard to get.

Each release is published under several tags, so you choose how much moves when you upgrade:

Tag Points at
8 The newest release in that major version. The default here.
8.1 The newest patch of that minor version.
8.1.0 Exactly that release.
latest The newest release of all, major version bumps included.

Compose pulls them for you:

docker compose -f docker-compose.prod.yml up -d

First boot takes 40 to 80 seconds while database migrations run. The backend is on port 8000 and the dashboard on 5173, both plain HTTP at this point.

4. Put TLS in front

Do not expose those ports directly. You need two proxy hosts, each on its own hostname, each terminating TLS. If you do not already run a proxy, use the Caddy variant instead, which does it for you:

curl -O https://raw.githubusercontent.com/maelqo/scripts/main/aiflow/config/docker-compose.caddy.yml
curl -O https://raw.githubusercontent.com/maelqo/scripts/main/aiflow/config/Caddyfile.example
mv Caddyfile.example Caddyfile   # then edit in your two hostnames
docker compose -f docker-compose.caddy.yml up -d

If you do already run one, see Your own reverse proxy for the requirements.

Then continue to First steps.

Coolify

Coolify is an open-source, self-hosted PaaS: a web UI for deploying containers on your own server, with certificates handled for you. Self-hosting it is free; you never need a Coolify account.

  1. Install Coolify on your server:

    curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash
    

    Its own dashboard comes up on port 8000 of that server.

  2. Create the root user. Visit http://<server-ip>:8000 and set it up. This first step is browser-only and cannot be scripted, which is why Coolify installs are not fully automatable.

  3. New Resource → Docker Compose, and paste the contents of docker-compose.coolify.yml:

    curl -O https://raw.githubusercontent.com/maelqo/scripts/main/aiflow/config/docker-compose.coolify.yml
    

    Use the Coolify compose file, not the standard one

    docker-compose.prod.yml publishes host ports. Coolify routes to services over its own internal network instead, and binding those ports collides with Coolify's own dashboard on port 8000. You will see port is already allocated. The Coolify file declares no host ports, on purpose.

  4. Set environment variables in Coolify's UI rather than an .env file: SECRET_KEY, GEMINI_API_KEY, PUBLIC_BASE_URL, and your licence and Twilio values if you have them.

  5. Attach your domains under each service's Domains tab: your API subdomain to the backend on port 8000, your admin subdomain to the dashboard on port 80. Coolify issues and renews the certificates.

  6. Deploy. Then create your first admin using Coolify's Execute Command action on the backend service, with the command from First steps.

To upgrade later, redeploy from the Coolify dashboard, or configure it to redeploy automatically when a new image is published.

Your own reverse proxy

If you already run Nginx, Traefik, an existing Caddy, or a load balancer, point it at the containers from Docker Compose and satisfy this checklist:

  • Two hosts, one to the backend on port 8000, one to the dashboard on port 8080 inside the Compose network (5173 if you mapped it to the host).
  • TLS on both, with a valid certificate for each hostname.
  • On the backend host, forward Upgrade and Connection: upgrade.

That last one matters more than it looks. The widget's live conversation and Twilio's Media Streams are both WebSockets. Without those headers they fail to upgrade silently: the dashboard loads, the API answers, and calls just never produce audio.

First steps

Create your first admin

This is the only account you create from the command line; everyone else is invited from the dashboard.

docker compose -f docker-compose.prod.yml exec backend \
  python -m scripts.create_admin you@example.com 'a-strong-password'

Then sign in at your admin subdomain.

Check it is healthy

curl -sf https://aiflow-api.example.com/health

See Quickstart for your first API call.

Try it with sample data

Two seed scripts, both safe to re-run:

# One demo agent, enough to embed the widget and talk to it.
docker compose -f docker-compose.prod.yml exec backend \
  python -m scripts.seed_demo_agent

# "Riverside Home Services": two linked agents, an orchestrator, a knowledge
# document, seven skills, and sample conversations, including a returning
# customer so cross-session memory is visible straight away.
docker compose -f docker-compose.prod.yml exec backend \
  python -m scripts.seed_riverside_demo

The Riverside demo leaves its sample conversations alone on later runs, so anything you change while exploring stays put.

Command reference

Everything below runs inside the backend container. The prefix is always the same, so it is written once here and omitted afterwards:

docker compose -f docker-compose.prod.yml exec backend <command>

On Coolify, use the Execute Command action on the backend service with the same command. If you are running the backend directly from source rather than in Docker, run the same python -m ... from the backend/ directory.

Command What it does
python -m scripts.create_admin EMAIL PASSWORD [ROLE] Creates an admin. ROLE defaults to owner. Invite later admins from the dashboard.
python -m scripts.seed_demo_agent Creates one demo agent for trying the widget.
python -m scripts.seed_riverside_demo Creates the connected sample business described above.
python -m scripts.seed_staff --staff "Name:Role:email:phone" Fills the staff roster that verify_staff_member checks against. Repeatable. See Staff roster.
python -m scripts.backup --output-dir ./backups Backs up the database, uploaded documents, and Orchestrator sessions. See Backup and restore.
python -m scripts.restore --db-backup FILE Restores from a backup. Stop the backend first.
python -m scripts.export_public_openapi PATH Writes this API's OpenAPI schema to a file.
python -m scripts.dev_license Prints a throwaway fully-featured licence, for evaluating paid features locally. Never for production.

And on the host, not in the container:

Command What it does
docker compose -f docker-compose.prod.yml logs -f backend Follows the backend log.
docker compose -f docker-compose.prod.yml ps Shows whether each container is healthy.
docker compose -f docker-compose.prod.yml restart backend Restarts just the backend.
docker compose -f docker-compose.prod.yml down Stops everything. Your data volume survives.

Upgrading

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

That is the whole procedure. Database migrations run automatically when the backend starts.

Choose how much you want to move. Set AIFLOW_VERSION in .env:

Value Behaviour
8 Latest release in that major version. The default, and never crosses a breaking change.
8.1 Latest patch of that minor version.
8.1.0 Exactly that release. Nothing moves until you change it.

Conversations in progress are dropped

Restarting the backend ends any live call or chat immediately. There is no graceful drain. The next startup marks them failed rather than leaving them stuck, but the caller's experience is an abrupt disconnect. Upgrade outside business hours if that matters.

Back up first. A restore is much easier than a rollback:

docker compose -f docker-compose.prod.yml exec backend \
  python -m scripts.backup --output-dir /app/backend/backups

Using Postgres instead of SQLite

SQLite is the default and needs nothing. It is a real choice, not a placeholder: it runs in WAL mode with a busy timeout, and handles a small deployment's concurrent writes comfortably.

Move to Postgres when your own backup or failover tooling is built around it, or when call volume outgrows a single file. It is an additive overlay, not a replacement file:

docker compose -f docker-compose.prod.yml -f docker-compose.postgres.yml up -d

Troubleshooting

The containers start, then the backend keeps restarting

Check the log first:

docker compose -f docker-compose.prod.yml logs backend | tail -50

The usual cause is SECRET_KEY being unset or left at its placeholder. The backend refuses to start rather than run with a guessable signing key.

The dashboard loads but everything says "network error"

The dashboard is told where the backend is at container start, from PUBLIC_BASE_URL. If that is wrong, missing, or still http://localhost:8000, the browser tries to reach a backend that is not there.

Fix PUBLIC_BASE_URL in .env, then recreate the admin container so it picks the value up:

docker compose -f docker-compose.prod.yml up -d --force-recreate admin

Calls connect but there is no audio, and the widget never starts talking

Your reverse proxy is not forwarding WebSocket upgrade headers. Everything else works, which is what makes this one confusing. See Your own reverse proxy.

Features are missing, and their endpoints return 404

That is expected on an unlicensed deployment. A capability you have not licensed is not mounted at all, so it returns 404 rather than 403: confirming that a feature exists but is withheld would itself be information.

Check what your deployment currently has:

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://aiflow-api.example.com/api/v1/features

mode tells you which tier you are on. If you set a licence key and still see demo, the key did not verify: check for a truncated paste, and check the server clock, since activation allows only a few minutes of drift.

It was working, then dropped to reduced functionality

A licence that stops verifying does not cut service immediately. The deployment keeps its last known-good entitlements for 14 days, logging loudly, and mode reads grace. Fix the key or the clock inside that window.

One instance refuses new conversations while others are fine

You are running more instances than your licence permits. The extra instance still serves its dashboard, with a banner saying so, and finishes what it already has, but starts nothing new. Either stop an instance or move up a tier. GET /api/v1/features reports instances_live against deployments_permitted.

Coolify says port is already allocated

You pasted docker-compose.prod.yml instead of docker-compose.coolify.yml. See the warning in Coolify.

A deeper check

Signed in as an admin, this runs real checks against the database and configured providers rather than just answering "up":

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  "https://aiflow-api.example.com/health?deep=true"

Where to go next