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

# Local installation

> Run the full HAI-Co² stack on your machine with Docker Compose: prerequisites, a one-command bring-up, configuration, and first login.

HAI-Co² runs locally as a **Docker Compose** stack, so you do not install Python or Node on the
host. Everything lives under [`deployment/local/`](https://github.com/petrosrapto/HAICO/blob/main/deployment/local), and a single script brings the whole
system up, applies database migrations, and health-checks every service.

## Prerequisites

The Dockerized flow needs only one thing:

* **Docker Engine 24+** with the **Compose v2** plugin. Verify with `docker --version` and `docker compose version`.

The images carry their own runtimes (backend on Python 3.13, frontend on Node 24), so no host
Python or Node is required.

> [!NOTE]
>
> The local frontend image is a **production build** (`node server.js`), not `next dev`, so there is
> **no hot reload**: after editing backend or frontend source you must rebuild with
> `docker compose up -d --build` (or re-run the deploy script).

## Quickstart

**1. Get the code and enter the local deployment**

```bash theme={null}
git clone https://github.com/petrosrapto/HAICO.git
cd HAICO/deployment/local
chmod +x scripts/*.sh
```

**2. Create the local config files**

`deploy.sh` copies them from the examples on first run, or do it yourself:

```bash theme={null}
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
cp backend/config.local.override.example.yaml backend/config.local.override.yaml
```

**3. Set the essentials in backend/.env**

Set a long random `JWT_SECRET`, an `ADMIN_DEFAULT_PASSWORD` (this seeds the `admin` login on the
first migration), and at least one LLM key (e.g. `OPENAI_API_KEY`). Leave `RECAPTCHA_SECRET_KEY`
and `SMTP_HOST` empty so local dev skips captcha and email verification.

**4. Bring the stack up**

```bash theme={null}
./scripts/deploy.sh
```

This sources both `.env` files, runs `docker compose up -d --build`, then health-checks Postgres,
the backend, and the frontend. The equivalent manual command is `docker compose up -d --build`.

**5. Log in**

Open [http://localhost:3000](http://localhost:3000), sign in at `/login` with username **`admin`**
and the `ADMIN_DEFAULT_PASSWORD` you set (this account is pre-verified), then open the Studio at
`/app`.

## What comes up

`deploy.sh` runs the Compose file at [`deployment/local/docker-compose.yml`](https://github.com/petrosrapto/HAICO/blob/main/deployment/local/docker-compose.yml), which starts four services:

| Service | Image / build | URL | Notes |
| - | - | - | - |
| `postgres` | `postgres:15-alpine` | `localhost:5432` | Workspace + agent memory; volume `haico_pg_data`; backend waits on its `pg_isready` healthcheck |
| `backend` | builds [`backend/Dockerfile`](https://github.com/petrosrapto/HAICO/blob/main/backend/Dockerfile) | [http://localhost:8000](http://localhost:8000) | FastAPI; Swagger at `/docs`, health at `/health`, build identity at `/api/version` |
| `frontend` | builds [`frontend/Dockerfile`](https://github.com/petrosrapto/HAICO/blob/main/frontend/Dockerfile) | [http://localhost:3000](http://localhost:3000) | Next.js production build; proxies `/api` to the backend |
| `phoenix` | `arizephoenix/phoenix` | [http://localhost:6006](http://localhost:6006) | Agent trace UI (login `admin@localhost` / `haicoadmin`); optional |

On first start the backend's [`entrypoint.sh`](https://github.com/petrosrapto/HAICO/blob/main/backend/entrypoint.sh) waits for Postgres, runs `alembic upgrade head` (creating the
schema and seeding the admin user), runs `python -m app.db.init_checkpointer` (creating the
LangGraph checkpointer tables), and then starts uvicorn.

## Configuration

Settings resolve in the order **env var → `.env` → YAML → defaults** (see [`backend/app/core/config.py`](https://github.com/petrosrapto/HAICO/blob/main/backend/app/core/config.py)):

* **`backend/.env`** (secrets): `JWT_SECRET`, `ADMIN_DEFAULT_PASSWORD`, LLM API keys, optional `SMTP_*`, `RECAPTCHA_SECRET_KEY`, `GOOGLE_OAUTH_CLIENT_ID`. Compose assembles `DATABASE_URL` from the `POSTGRES_*` values, so you do not set it directly.
* **`frontend/.env`**: `API_PROXY_TARGET` (default `http://backend:8000`) and the `NEXT_PUBLIC_*` keys, baked into the image **at build time** (so changing them needs a rebuild).
* **`deployment/local/backend/config.local.yaml`** (non-secret, tracked and shared): the default LLM `provider`/`model`, CORS origins, and `observability.provider` (`none` to disable Phoenix/LangSmith tracing). Secrets never come from YAML.
* **`deployment/local/backend/config.local.override.yaml`** (yours, gitignored): your own settings, merged over `config.local.yaml` key by key, so it only needs the keys you change and they can never be committed by mistake. `deploy.sh` creates it from the tracked `config.local.override.example.yaml`, and Compose mounts it and names it in `CONFIG_OVERRIDE_PATH`. Restart the backend after an edit (`docker compose restart backend`).
* **A self-hosted default model**: in your override, set `llm.provider: openai`, the server's model name in `llm.model`, and its OpenAI-compatible address (Ollama, vLLM) in `llm.endpoint_url`, such as `http://host.docker.internal:11434/v1` for Ollama on your machine. If the server needs a key, put it in `backend/.env` as `DEFAULT_LLM_API_KEY`. That address and key serve only the default model, which is what an API request that names no model runs on. Requests for any other model never go there, the Studio always sends the model picked in its menu, and the OpenAI key is never sent there.

> [!WARNING]
>
> **`CONFIG_PATH` gotcha.** The YAML is found by scanning a hardcoded `CONFIG_PATHS` list; an explicit
> `CONFIG_PATH` env var is prepended and wins. The Compose file sets `CONFIG_PATH=/app/config.local.yaml`
> and mounts the file there, so it resolves. If you craft a custom container where `CONFIG_PATH` points
> at a path that does not exist, none of the relative fallbacks match the container filesystem and the
> YAML is **silently ignored**, falling back to `Settings` defaults.

## First login and accounts

* **Seeded admin** is the practical way in: the first migration creates user `admin` (role admin, `email_verified=true`) using your `ADMIN_DEFAULT_PASSWORD`.
* **Self-registration** (`/login` → register) enforces email verification before login, so without `SMTP_*` configured a self-registered local user cannot log in. Use the admin account, or configure SMTP.
* **Captcha** is skipped automatically when `RECAPTCHA_SECRET_KEY` is empty.

> [!WARNING]
>
> There is also an optional `ADMIN_DEFAULT_API_TOKEN`, commented out in `.env.example`. Setting it to a full `haico_pat_…` string installs a personal access token for the admin account on the first migration, so a script can drive the API with no browser ([ADR-0006](/docs/adr/0006-programmatic-api-access.md), migration `0017`). It exists for throwaway databases — CI, or an automated batch run — and should stay unset in dev and production, where a static long-lived credential is the thing PATs exist to avoid. The [SLURM batch runs](/docs/slurm-experiments.md) do not need it: each job generates its own and stores nothing.
>
> Set it, restart the backend so the migration runs, and check it end to end:
>
> ```bash theme={null}
> python -c "import secrets; print('haico_pat_' + secrets.token_urlsafe(32))"   # put this in backend/.env
> docker compose build backend && docker compose up -d backend                  # 0017 runs on start
>
> curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/auth/me       # 200, the admin user
> curl -X POST http://localhost:8000/api/keys -H "Authorization: Bearer $TOKEN"  # 403 by design
> ```
>
> The second call is expected to fail. A personal access token cannot mint another one — `require_session_user` rejects it — which is the boundary [ADR-0006](/docs/adr/0006-programmatic-api-access.md) draws between the interactive and programmatic planes.
>
> > [!NOTE]
> >
> > The migration seeds the key **only when the admin account already exists**, so `ADMIN_DEFAULT_PASSWORD` must have been set on the run that created the database. On an existing local database that already has an admin, setting the token and restarting is enough.
>
> `ADMIN_DEFAULT_PASSWORD` seeds the admin **only on the first migration** (empty database). Changing
> it later has no effect until you drop the Postgres volume (`./scripts/cleanup.sh --volumes`) and let
> migrations re-run.

## Day-to-day

```bash theme={null}
./scripts/deploy.sh --status      # health + container status
./scripts/deploy.sh --logs        # follow logs
./scripts/deploy.sh --restart     # restart the stack
docker compose logs -f backend    # one service (run from deployment/local)
docker compose exec backend bash  # shell into a container
docker compose up -d --build      # rebuild after a source change (no hot reload)
```

## Resetting

```bash theme={null}
./scripts/cleanup.sh              # stop, keep data + images (= --down)
./scripts/cleanup.sh --volumes   # also drop haico_pg_data (re-seeds admin on next up)
./scripts/cleanup.sh --images    # also remove the built images
./scripts/cleanup.sh --all       # everything
```

For how this same stack is built and shipped to the dev and production servers, see
[Deployment](/docs/deployment.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.