TaifaSupport Docs
Self-hosting

Install

Running TaifaSupport on an Institution's own hardware, under its own domain, with its own data.

For an Institution running TaifaSupport on its own hardware, under its own domain, with its own data. No part of this install has to reach our infrastructure, and with the AI profile enabled no part of it has to reach the internet at all.

Everything here assumes EDITION=self_hosted, which is the default.

What you need

Hardware

UseCPURAMDisk
Evaluation, one site, a handful of agents2 cores4 GB20 GB
Production, one busy portal, 10 to 30 agents4 cores8 GB100 GB SSD
Production plus local AI (gemma2:2b)4 cores16 GB120 GB SSD
Production plus local AI (gemma2:9b)8 cores32 GB, or 16 GB with an 8 GB GPU150 GB SSD

Disk is dominated by page_views, which is the highest-volume table and grows with traffic rather than with agents. A portal doing 50,000 pageviews a day writes roughly 1 GB a month before indexes. Plan the disk against traffic, not against headcount.

Software

  • A Linux host. Ubuntu 22.04 or 24.04 LTS is what we test on.
  • Docker Engine 24 or newer and the Compose plugin.
  • A DNS name pointing at the host, if anything other than you is going to reach it.

Nothing else. PostgreSQL, Redis and nginx all come in the stack unless you choose to supply your own.

Install

git clone <repository> /opt/taifa-support
cd /opt/taifa-support
 
cp .env.example .env

Three values must change before this is used for anything real:

.env
# A long random value. Rotating it later signs every agent out.
SECRET_KEY=<openssl rand -hex 32>
 
POSTGRES_PASSWORD=<something long and generated, not typed>
PUBLIC_URL=https://support.yourministry.go.ke

Also set CORS_ORIGINS to the same host as PUBLIC_URL, and COOKIE_SECURE=true if you are serving over https, which you should be.

Bring it up:

make up          # builds and starts postgres, redis, backend, worker,
                 # frontend, nginx, and the widget builder
make migrate     # creates the 56 tables

make migrate is a separate step on purpose. The stack starting and the schema changing are different events, and a deployment that silently migrates on boot is one that can corrupt a database because a container restarted at the wrong moment.

Open PUBLIC_URL. Because this is a self-hosted install with no Institution yet, you land on the first-run wizard.

Try it with demo data first

If you would rather see the product full before pointing it at a real portal:

make seed

This creates one demo Institution with 40 visitors, live sessions, conversations, tickets and a knowledge base. It is idempotent, and it only ever touches the Institution whose slug is demo-lands, so it is safe to run on an install that already has a real tenant.

The port map

Every service binds 127.0.0.1:<port>. nginx is the only thing that answers from outside the host. This is not negotiable and it is worth re-checking after any compose edit.

ServiceBindsWhat it is
nginxHTTP_PORT:80 (8080 dev, 80 and 443 prod)The only service published on all interfaces, and the only one reachable from off the host. Terminates TLS in production and proxies everything else.
frontend127.0.0.1:3000SvelteKit agent console on adapter-node. Served through nginx at /.
backend127.0.0.1:8000FastAPI. Served through nginx at /v1 and /v1/ws.
postgres127.0.0.1:5432The 56 tables. Never published beyond loopback.
redis127.0.0.1:6379Presence, the websocket fan-out and the Celery queue.
workernot publishedCelery: session sweeps, triggers, embeddings, outbound mail.
widgetnot publishedA one-shot builder that writes /widget.js into a shared volume, then exits.
ollama127.0.0.1:11434Optional, behind --profile ollama. The local model host.
minio127.0.0.1:9000, :9001Optional, behind --profile minio. Only for trying object storage without an account.

The check worth running after every compose edit:

docker compose -f docker-compose.prod.yml config --format json \
  | python3 -c 'import json,sys
for n,s in json.load(sys.stdin)["services"].items():
    for p in s.get("ports",[]):
        print(n, p.get("host_ip","0.0.0.0"), p.get("published"))'

Anything other than nginx printing 0.0.0.0 is a bug in the compose file.

Production hardening

A production install adds three things to the above.

Firewall

ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw enable

Everything in the stack already binds loopback, so this is defence in depth rather than the only thing standing between PostgreSQL and the internet. A misconfigured compose file has happened to everyone, and this is the layer that survives it.

A non-login system user

useradd -r -m -d /opt/taifa-support -s /usr/sbin/nologin taifa
usermod -aG docker taifa

TLS

The production nginx terminates TLS itself. Issue the certificate before the first up, because nginx will not start if the paths do not exist.

apt install -y certbot
certbot certonly --standalone -d support.yourministry.go.ke

You do not edit the vhost. It is rendered from a template, and the hostname comes from PUBLIC_ORIGIN in your .env:

make nginx-config

That fills the placeholder into infra/nginx/generated/, then runs nginx -t on the result in a throwaway container, so a bad vhost is caught before anything is asked to serve it. make deploy does the same thing on your behalf.

Renewal, once nginx is up, uses the webroot rather than standalone so it does not need port 80 to itself. Run this once to move an existing certificate over, and the deploy hook keeps nginx picking up each new one:

certbot certonly --webroot -w /var/www/certbot -d support.yourministry.go.ke \
  --cert-name support.yourministry.go.ke

Starting production

docker-compose.prod.yml is standalone, not an override. Overrides merge in ways that are hard to predict from reading either half, and this is the one file where a mistake exposes a database to the internet.

docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml run --rm backend alembic upgrade head
docker compose -f docker-compose.prod.yml up -d

Verify before telling anyone it is live:

docker compose -f docker-compose.prod.yml ps          # everything up, nothing restarting
curl -fsS https://support.yourministry.go.ke/health
curl -fsSI https://support.yourministry.go.ke/widget.js | head -5

Routine deployment

From the checkout, on the server:

make deploy

which is:

docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml run --rm backend alembic upgrade head
docker compose -f docker-compose.prod.yml up -d
docker image prune -f

The order is the point:

  • Build first. A syntax error fails before anything is taken down.
  • Migrate before restarting. New code must never meet an old schema. Migrations are additive within a minor release, so the old containers keep serving correctly while this runs.
  • Prune last. If the migration step fails, the previous image is still on the host and a rollback is one git checkout away.

Take a dump before every deploy, without exception. See backups and upgrades.

Next

On this page