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

# Self-hosting

> Run the EgisAI control plane inside your own network. A reference Docker Compose stack, the settings that matter, what works without internet access, and a preflight command that answers the questions you would otherwise only think to ask after something broke.

Some organizations cannot send prompt metadata to a vendor-run service —
a defense contractor, a hospital group, a bank's model-risk function.
For them, governance that requires a SaaS control plane is governance
they cannot buy.

So the control plane runs in your network. Same image, same schema,
same governance loop as the hosted product. Postgres and Redis are the
only dependencies, and neither the SDK nor the backend calls out to us.

## Evaluate it in fifteen minutes

```bash theme={null}
git clone https://github.com/EgisLabs/egisai-platform.git
cd egisai-platform/deploy/compose

cp .env.example .env
./generate-secrets.sh >> .env     # or fill them in by hand

docker compose up -d              # Postgres, Redis, migrations, service
docker compose run --rm bootstrap # creates your first login
```

Then open `http://localhost:8008`.

`bootstrap` is interactive by default and asks for an email, a name,
an organization, and a password — press enter at the password prompt
and it generates a strong one and prints it once.

<Note>
  `bootstrap` needs no mail server. Every other route into the platform
  sends something — a verification code at signup, a link in an
  invitation — which is right for the hosted product and wrong for a
  machine you brought up ten minutes ago. Without this command a fresh
  deployment is running and unreachable.
</Note>

## The four settings that actually matter

Everything else has a working default. These four do not, and three of
them fail quietly.

| Setting                                    | What breaks without it                                                                                                                                                                                                                            |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `JWT_SECRET_KEY`, `JWT_REFRESH_SECRET_KEY` | The service refuses to start. The loud one.                                                                                                                                                                                                       |
| `PUBLIC_BASE_URL`                          | Every OAuth callback, inbound webhook, SAML login and SCIM client is told to come back to `localhost`. Connectors never finish authorizing and webhooks never arrive — separately, and much later.                                                |
| `FIELD_ENCRYPTION_KEY`                     | Stored provider keys and connector tokens are encrypted with a key derived from `JWT_SECRET_KEY`. The day someone rotates that secret — normal hygiene — every one of them becomes permanently unreadable.                                        |
| `REDIS_URL`                                | Correct on one replica. On two, each keeps its own event bus, rate-limit counters, and SSE tickets: operators see only the events from whichever replica holds their connection, and rate limits are effectively multiplied by the replica count. |

`generate-secrets.sh` produces the two JWT secrets, the field
encryption key, and a Postgres password, each from a cryptographically
secure source. Do not reuse a value across two deployments, and do not
copy one out of a README — a signing key someone else has read is a
signing key that mints sessions for any user in your deployment.

<Warning>
  Set `FIELD_ENCRYPTION_KEY` **before** you store the first provider
  credential. There is no recovery path afterward: the plaintext was
  never kept anywhere, so a rotated `JWT_SECRET_KEY` turns every stored
  credential into unreadable bytes.
</Warning>

## Ask the deployment whether it is healthy

```bash theme={null}
docker compose run --rm doctor
```

Thirteen checks, grouped, each one either silent or explaining what you
lose and what to do:

```
EgisAI preflight

Core
  ok    Database reachable
  ok    Schema up to date
  ok    pgcrypto installed
  warn  An account exists
        No accounts yet, so there is nobody to log in as. Create the
        first organization and owner with `python -m app.bootstrap` —
        it needs no email transport.

Secrets and addresses
  ok    Session signing keys
  FAIL  Stored-credential encryption key
        FIELD_ENCRYPTION_KEY is not set, so stored provider keys and
        connector tokens are encrypted with a key derived from
        JWT_SECRET_KEY. The day somebody rotates the JWT secret —
        normal hygiene — every one of them becomes permanently
        unreadable. There is no recovery.
```

It exits non-zero when something is genuinely broken, so it composes
with a CI gate or a deploy step. Warnings alone are not a failure — an
air-gapped deployment correctly has several. Add `--strict` if you want
them to be, and `--json` for monitoring.

The same checks run at every boot and log a one-line summary, so a
deployment that drifts into a degraded state says so in its own logs
rather than waiting to be asked.

## What works with no internet access

The governance loop is local. Air-gapped, you keep:

* Every deterministic rule: `pii_scan`, `deny_regex`, `allow_model`,
  `max_prompt_chars`, `injection_scan`, the tool and MCP rules, rate
  and budget limits.
* PII detection and masking, including the NER model — it ships baked
  into the image rather than downloading at startup.
* The full audit trail, the dashboard, SSO, SCIM, and evidence export.

You lose the parts that are a model call by definition:

* `semantic_guard` rules and the LLM-backed anomaly judge, unless you
  point `LLM_BASE_URL` at a model you host yourself. Any OpenAI-
  compatible endpoint works — vLLM, Ollama, a private Azure OpenAI
  deployment.
* SaaS connectors (Google Workspace, Microsoft 365, Okta, Slack),
  which need to reach those vendors by construction.
* Outbound email, unless you have an internal SMTP relay. In-app
  notifications still work.

## Sending governance into your own tracing

Egis knows something your tracing does not — that a call was refused,
or that four card numbers were masked out of it — and by default the
only place to see it is the Egis dashboard. Set an OTLP endpoint and
every governed call also becomes a span in your own stack:

```bash theme={null}
EGIS_OTLP_ENDPOINT=http://otel-collector:4318/v1/traces
EGIS_OTLP_HEADERS=x-api-key=...        # optional
EGIS_OTLP_SERVICE_NAME=egisai          # optional
```

Spans follow the OpenTelemetry GenAI semantic conventions, so they
render as model calls in Datadog, Honeycomb, Grafana Tempo, or
anything else that speaks OTLP — rather than as an opaque vendor
event nobody has a view for.

The part that makes this worth turning on is the parenting. The SDK
stamps the ambient W3C trace and span id onto every event, so the
governance span becomes a **child of the span the call actually
happened inside**. A blocked call shows up inline in the trace your
engineer was already looking at, between the two spans it sat between,
marked as an error with the policy's reason on it. Today that same
engineer sees an unexplained gap.

<Note>
  Nothing on a span carries content. Prompt text, response text,
  matched values, and sanitized excerpts are all absent by
  construction — a collector is a third party, and the attributes are
  counts, verdicts, model names, and durations. The one string that
  comes close is the rule's own name, which you wrote.
</Note>

Install the exporter with the `otel` extra:

```bash theme={null}
pip install "egisai-backend[otel]"
```

Without it the setting is a no-op and the boot log says so by name,
rather than silently doing nothing.

## Going to production

The compose file is a reference, not a production topology. Before it
carries real traffic:

* **Terminate TLS in front of the service** and set `PUBLIC_BASE_URL`
  to the `https://` origin. Authorization codes and session cookies
  cross that URL.
* **Use managed Postgres** if you have one, and point `DATABASE_URL`
  at it. The compose Postgres has no backup story; yours does.
* **Do not scale to zero.** The workers are in-process asyncio loops
  that do real work on a timer — retention sweeps, rollups, intelligence
  refresh. A platform that suspends an idle container silently stops
  all of them.
* **Set `REDIS_URL` before adding a second replica**, for the reasons
  in the table above.
* **Run `doctor` after every config change.** It is cheap and it is the
  only thing that will tell you about the quiet failures.

Upgrades are `docker compose pull && docker compose up -d`. The
`migrate` job runs to completion before the service starts, so the
control plane never comes up against a schema it does not understand —
which otherwise surfaces as a 500 on one page and nowhere else.
