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/amd64andlinux/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.
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.
And to see every .env variable AiFlow reads, with defaults:
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:
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:
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:
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.
-
Install Coolify on your server:
Its own dashboard comes up on port 8000 of that server.
-
Create the root user. Visit
http://<server-ip>:8000and set it up. This first step is browser-only and cannot be scripted, which is why Coolify installs are not fully automatable. -
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.ymlUse the Coolify compose file, not the standard one
docker-compose.prod.ymlpublishes 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 seeport is already allocated. The Coolify file declares no host ports, on purpose. -
Set environment variables in Coolify's UI rather than an
.envfile:SECRET_KEY,GEMINI_API_KEY,PUBLIC_BASE_URL, and your licence and Twilio values if you have them. -
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 port80. Coolify issues and renews the certificates. -
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
UpgradeandConnection: 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¶
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:
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¶
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:
Troubleshooting¶
The containers start, then the backend keeps restarting¶
Check the log first:
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:
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:
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":
Where to go next¶
- Credentials: getting the Gemini, Twilio, Telegram, and email keys AiFlow uses.
- Quickstart: your first API call against the deployment you just built.
- Authentication: creating the API keys your own systems will use.
- Backup and restore: what is covered, and how to schedule it.
- Observability: logs, metrics, and what to monitor.