---
title: Self-hosting
description: Run Izri on your own infrastructure with Docker Compose — services, environment, and the runner you must not forget.
order: 40
---

# Self-hosting

Izri is open-core under AGPL-3.0. The four signals, the CLI, the MCP server, and the GitHub Action all run against a self-hosted instance with no license key.

## You need the runner

Start here, because it is the failure everyone hits.

Izri is **five services, not two**. The API and web app alone look like a working stack — the dashboard loads, you can create a project, you can trigger a run. But triggering a run only enqueues a Redis job. With no runner draining that queue, the run sits at `PENDING` forever and nothing ever tells you why.

The API does warn: a runner-presence watchdog logs a `WARN` once the queue stays non-empty for more than 60 seconds. If a run is stuck, check the API logs first.

## Services

| Service | Port | Purpose |
| --- | --- | --- |
| `web` | 3000 | React Router SSR dashboard |
| `api` | 4000 | Hono server, tRPC, webhooks |
| `ai-service` | 8000 | Python FastAPI — analysis and generation |
| `runner` | — | Drains the Redis queue and executes tests |
| `postgres` | 5433 | Primary datastore |
| `redis` | 6380 | Job queue |

Postgres is on **5433** and Redis on **6380**, not their defaults, so a self-hosted instance doesn't collide with a database already running on the host.

## Bring it up

```bash
git clone https://github.com/legendify-dev/izri.git
cd izri
cp .env.shared.example .env.shared
# edit .env.shared — see below
pnpm install
pnpm env:generate
docker compose up -d
```

`pnpm env:generate` is not optional. It writes the per-app `.env` files from `.env.shared`; without it auth, the database layer, and tRPC fail in confusing ways rather than reporting a missing variable.

## Environment

The variables that actually block a first run:

| Variable | Notes |
| --- | --- |
| `DATABASE_URL` | Postgres connection string. |
| `REDIS_URL` | Queue connection. |
| `API_URL` / `APP_URL` | Must match where the services are actually reachable. |
| `BETTER_AUTH_SECRET` | Change it. The example value is a placeholder. |
| `JWT_SECRET` | Change it too. |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | OAuth login. |
| `GITHUB_APP_ID` / `GITHUB_APP_PRIVATE_KEY` / `GITHUB_APP_WEBHOOK_SECRET` | Required for PR checks and the sticky comment. |
| `AI_PROVIDER` | `openrouter`, `openai`, `anthropic`, or `mock`. |

### The OAuth callback will bite you

The GitHub OAuth callback is derived from your configured app URL. If the URL your browser uses and the URL Izri is configured with disagree — a port mismatch is the usual cause — login fails as a silent redirect loop with no error message. Make them match exactly.

### AI provider

Set `AI_PROVIDER` explicitly. `mock` runs the deterministic layers with no external calls and no cost, which is the right choice for evaluating the stack. The semantic scope layer and LLM insights need a real provider.

## Point your tooling at it

Everything takes the same override:

```bash
export IZRI_API_URL=https://izri.internal.example.com
```

```yaml
- uses: legendify-dev/izri/apps/github-action@v1
  with:
    api-token: ${{ secrets.IZRI_API_TOKEN }}
    api-url: https://izri.internal.example.com
```

## Database migrations

Schema changes ship as Drizzle migrations. After pulling a new version:

```bash
pnpm db:migrate
```

Migrations are immutable once released — never hand-edit one.

## What self-hosting does not include

The visual signal's semantic diff is a paid capability. Self-hosted instances get the three deterministic signals plus visual capture and pixel diff. See [plans and quotas](/docs/reference/plans-and-quotas).

## Related

- [Troubleshooting](/docs/reference/troubleshooting) — stuck runs, 401s, login loops.
- [GitHub Action](/docs/install/github-action) — pointing CI at your instance.
