title: Install with Docker Compose
description: Docker Compose starts LQ.AI and its supporting services together — the reference deployment, and the installation method this site's steps are tested against.
url: https://legalquants.github.io/lq-ai/operate/install-docker-compose/
checked-against: 8c66c85a (2026-10-26)
status: draft
kind: how-to
sources: README.md, docs/adr/0026-document-ingestion-parser-and-docling.md, docs/adr/0036-bundled-object-store-rustfs.md, docker-compose.yml, docs/quickstart.md, .env.example, docs/security/encrypted-keys.md

## Quick Start

### In short

Docker Compose is the reference way to run LQ.AI: it starts eight services together on your own computer — the website, the API and gateway that handle requests and AI connections, two background workers, and three storage services. You'll need Docker and `git`, nothing else. Copy `.env.example` to `.env`, set four required secrets, then start the stack; the first run also builds the application images, so it takes longer than a later restart. Set the admin password with the CLI command in Step 3 and sign in, then create a test matter and try a skill — no AI-provider key is needed to install and sign in, only to get an answer from a model. Changing an environment variable afterwards needs the containers recreated, not just restarted. Running a second trial, using a model on your own computer, and turning on legal-research connectors are separate, opt-in steps covered further down.

**Prerequisites:** [Docker Desktop 4.x+](https://www.docker.com/products/docker-desktop) (or Docker Engine 24+ on Linux) and `git`. No other host tooling required — no Python, no Node, no language-specific runtimes. Plan for ~8 GB of free disk space and ~6 GB of RAM available to Docker.

### Step 1 — Clone and configure

```bash
git clone --recurse-submodules https://github.com/LegalQuants/lq-ai.git
cd lq-ai
cp .env.example .env
```

> `--recurse-submodules` pulls in the [LegalQuants/lq-skills](https://github.com/LegalQuants/lq-skills) community skills catalog (30+ skills authored by lawyer-builders across 17+ jurisdictions). If you forgot the flag, run `git submodule update --init --recursive` from inside the cloned repo.

Open `.env` and set the four required secrets (the file explains each one):

- `POSTGRES_PASSWORD` — any long random string
- `OBJECT_STORE_SECRET_KEY` — any long random string
- `LQ_AI_GATEWAY_KEY` — any long random string
- `JWT_SECRET` — any long random string

Generate all four at once with Python 3 (`python3`), labelled and ready to paste into `.env`:

```bash
python3 -c 'import secrets; [print(f"{name}={secrets.token_urlsafe(32)}") for name in ("POSTGRES_PASSWORD","OBJECT_STORE_SECRET_KEY","LQ_AI_GATEWAY_KEY","JWT_SECRET")]'
```

Provider API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) are optional at this step. The stack starts without any; inference calls return "no provider configured" until you add at least one. Two paths add a key: set it in `.env` and restart the gateway, or — once `LQ_AI_GATEWAY_MASTER_KEY` is configured — add it at runtime through the admin provider-keys surface (`/api/v1/admin/provider-keys`), which encrypts the key into `gateway.yaml` and hot-applies it to the live gateway with no restart.

Upgrading an installation that used the bundled MinIO service? Follow the
[MinIO to RustFS migration runbook](https://github.com/LegalQuants/lq-ai/blob/8c66c85a/docs/runbooks/minio-to-rustfs.md) before
starting the new stack.

### Step 2 — Start the stack

```bash
docker compose up -d
```

Eight services start: postgres, redis, rustfs, gateway, api, ingest-worker, arq-worker, web. The api container runs `alembic upgrade head` automatically as the first step of its entrypoint, so a fresh deployment lands a fully-migrated schema before uvicorn accepts traffic (the arq-worker and ingest-worker skip migrations and wait for the api to be healthy). Wait ~60 seconds for healthchecks, then confirm:

```bash
docker compose ps   # all 8 services should show "healthy" or "running"
```

> **First-run admin password.** On the first start (and only the first start), the api container's startup logs an auto-generated admin password — a one-time secret you can use if you skip Step 3. Grep `docker compose logs api` for `First-run admin password` to retrieve it. Most operators ignore this and just run Step 3 below, which sets a known password directly.

### Step 3 — Set the admin password

```bash
docker exec -w /app lq-ai-api-1 python -m app.cli reset-admin-password \
  --email admin@lq.ai \
  --password 'LQ-AI-smoke-test-Pw1!' \
  --no-force-change
```

This sets a known password for the bootstrap admin account so you can log in immediately without the must-change-password gate. Use a stronger password for anything beyond a local evaluation.

> If `docker exec` reports the container isn't found, run `docker compose ps` to see the actual container name on your setup — some Docker Compose versions use `lq-ai_api_1` (underscores) instead of `lq-ai-api-1` (hyphens). The `docker compose exec api python -m app.cli ...` form also works on most setups if you prefer it.

### Step 4 — Log in

Navigate to `http://localhost:3000/lq-ai/login` and sign in:

- **Email:** `admin@lq.ai`
- **Password:** `LQ-AI-smoke-test-Pw1!` (or whatever you set in step 3)

Other endpoints available after the stack is up:

```
LQ.AI app:          http://localhost:3000/lq-ai
Backend API docs:   http://localhost:8000/docs   (Swagger UI)
Backend API docs:   http://localhost:8000/redoc  (ReDoc)
Inference Gateway:  http://localhost:8001/docs
```

### Step 5 — First 5 minutes

After login you land on the LQ.AI home. Three good starting points:

- Click **Learn** in the top bar to take the interactive tour — a set of playgrounds (the count grows with releases) walks through the architecture, the request lifecycle, the tier system, what the model sees, where your data lives, and how to author a skill. This is the fastest orientation for both evaluators and new contributors. (`http://localhost:3000/lq-ai/learn`)
- Click **Skills** to browse the 10 built-in skills. Each is inspectable: click any skill to read the `SKILL.md` driving it.
- Click **+ New matter** to create your first project workspace and attach a document to try a skill against.

---

### Providers and air-gapped deployments

LQ.AI ships with four provider adapters: **Anthropic** (Claude), **OpenAI** (GPT models + text-embedding-3-*), **Azure OpenAI** (M2-E1 — unblocks Azure-tenant enterprise deployments via the operator's existing Azure agreement), and **Ollama** (local). Any OpenAI-compatible local endpoint (vLLM, llama.cpp wrappers, LM Studio) works through the OpenAI adapter with a custom `base_url`. The AWS Bedrock compatible-API implementation supports explicit Messages, Chat Completions and stateless Responses transports; [live AWS verification is deferred beyond 0.9.1](https://github.com/LegalQuants/lq-ai/blob/8c66c85a/docs/bedrock-verification.md). Google Vertex AI remains on the deferred-enhancement list (DE-034) and welcomes community contribution — see [docs/HONEST-STATE.md](/lq-ai/trust/published-gaps/) for the current shipped-vs-deferred catalog.

Multiple providers can run in parallel: operators declare `model_aliases` in `gateway.yaml` that resolve to specific provider+model pairs with fallback chains. A request for `smart` might primary-route to `anthropic-prod/claude-opus-4-7` and fall back to `openai-prod/gpt-5` on failure; the routing log captures which target actually handled the request. Only the gateway uses provider keys (for outbound provider authentication); the api/ service never stores them. A key entered through the admin provider-keys surface passes through the backend to the gateway once and is then encrypted at rest in `gateway.yaml` (Fernet AES-128-CBC + HMAC-SHA256 per `gateway/app/secrets.py`). A key configured in `.env` (including one written by the desktop first-run wizard) stays in `.env` as plaintext, and switching that provider to a runtime key does not remove the older `.env` copy — the defense-in-depth around the highest-value secret in the deployment still depends on how the operator handles `.env`.

### Enabling legal-research connectors (case-law / MCP) — operator opt-in

External tool connectors (CourtListener case-law, MCP servers) are **off by default** — the gateway makes no outbound tool calls until an operator explicitly enables a `tool_providers` entry. This is deliberate: external tool egress is operator-controlled (ADR [0014](https://github.com/LegalQuants/lq-ai/blob/8c66c85a/docs/adr/0014-gateway-egress-boundary-for-tool-providers.md)). To turn on **case-law research**:

1. In `.env`, set `COURTLISTENER_API_TOKEN` to a token from [courtlistener.com/help/api](https://www.courtlistener.com/help/api/) (the `docker-compose.yml` gateway service forwards this variable into the gateway container).
2. In `gateway.yaml`, uncomment the `tool_providers:` → `courtlistener-prod` block (the template is in [`gateway.yaml.example`](https://github.com/LegalQuants/lq-ai/blob/8c66c85a/gateway.yaml.example); `api_key_env: COURTLISTENER_API_TOKEN`).
3. Recreate the gateway so it picks up the new `.env` value: `docker compose up -d --force-recreate gateway` (a plain `docker compose restart` keeps the container's old environment). Confirm as a signed-in user — the capabilities route requires a bearer token (from `POST /api/v1/auth/login`): `curl -s -H "Authorization: Bearer $TOKEN" localhost:8000/api/v1/research/capabilities` → `"enabled": true`. An unauthenticated call returns `401`, which is not a research-setup failure.

Once enabled, case-law lookups run through the governed chat tool-loop (every call tier-gated, audited to `tool_egress_log` as counts/types only, and surfaced to the user as "Sources consulted" provenance). MCP connectors follow the same opt-in pattern (register via the `/api/v1/admin/mcp` surface + per-user OAuth). See [HONEST-STATE.md §5.5](/lq-ai/trust/published-gaps/).

For a **fully air-gapped deployment** (no outbound network calls), start the stack with the `local` profile to add the Ollama sidecar for local inference:

```bash
docker compose --profile local up -d
```

In this mode, inference runs entirely on the local host via Ollama. The Inference Gateway is the only egress point in the stack — verify in `gateway/app/router.py`. Provider keys are used only inside the Gateway (runtime-managed keys are encrypted at rest per `gateway/app/secrets.py`; keys set in `.env` stay in `.env`); the API service never stores them.

Before you blame the model, check which Ollama the gateway is actually pointed at — inside the gateway container, `localhost` is the container, not your machine. `OLLAMA_BASE_URL` picks the target: `http://ollama:11434` reaches this Compose sidecar and is what `gateway.yaml.example` falls back to when the variable is unset, but `.env.example` ships `OLLAMA_BASE_URL=http://host.docker.internal:11434` by default, so a `.env` copied from it points at Ollama on the host until you switch that line for `--profile local`. `host.docker.internal` is a Docker Desktop convenience; `docker-compose.yml` adds no `extra_hosts` mapping for it, so on plain Linux Docker Engine either add one yourself or point at the host's private-range IP directly — the gateway's egress policy accepts plaintext `http` to loopback and RFC1918 addresses as well as these named hosts (`gateway/app/providers/base_url_policy.py`). Either way, starting the profile brings up an empty Ollama and pulls no models on its own.

---

### Troubleshooting

**`docker compose up` fails immediately** — confirm Docker Desktop is running and has at least 6 GB RAM allocated. On macOS: Docker Desktop → Settings → Resources. Also confirm all four required `.env` variables are set (the compose file requires them and will exit with an error message naming the missing variable).

**Fewer than 8 services are healthy** — wait 60 seconds; the postgres healthcheck can take time on first boot. If services are still unhealthy after that: `docker compose logs <service-name>` to see what is failing.

**Login rejected / password not accepted** — re-run step 3 (the reset command is idempotent). Confirm you are using the login URL `http://localhost:3000/lq-ai/login`, not `http://localhost:3000`.

**Inference returns "no provider configured"** — add at least one provider key. Either set it in `.env` (e.g., `ANTHROPIC_API_KEY=sk-...`) and recreate the gateway with `docker compose up -d --force-recreate gateway` — the gateway reads environment provider keys at startup, and a plain `docker compose restart gateway` keeps the container's old environment, so the new value never arrives. This only takes effect if the provider's `gateway.yaml` entry still uses `api_key_env` (an entry already switched to an encrypted runtime key ignores the environment variable). The same recreate step applies to any other forwarded environment value, such as `OLLAMA_BASE_URL`. Or, with `LQ_AI_GATEWAY_MASTER_KEY` set, add it at runtime via the admin provider-keys surface (`/api/v1/admin/provider-keys`), which hot-applies without a restart.

**A host port is already in use** (`bind: address already in use`) — every host-side port the stack uses is configurable via a `*_HOST_PORT` variable in `.env` (the compose file reads them; see `docker-compose.yml`). Override the colliding port and `docker compose up -d` again. The defaults shipped in `.env.example`:

```
POSTGRES_HOST_PORT=5432    # collides with Homebrew Postgres on macOS — set to 15432 if needed
WEB_HOST_PORT=3000         # change if another web app (Next.js dev, etc.) holds :3000
API_HOST_PORT=8000         # change if a host service (Django, FastAPI) holds :8000
GATEWAY_HOST_PORT=8001     # change if another service holds :8001
REDIS_HOST_PORT=6379       # change if a host redis holds :6379
OBJECT_STORE_API_HOST_PORT=9000   # change if another service holds :9000
OBJECT_STORE_CONSOLE_HOST_PORT=9001  # change if Portainer/Console holds :9001
```

The most common Mac collision is `POSTGRES_HOST_PORT=5432` against a Homebrew Postgres. Either set `POSTGRES_HOST_PORT=15432` in your `.env` (the compose stack's internal traffic still uses 5432; services talk to each other unchanged — only the host-side mapping shifts), or stop the host postgres first (`brew services stop postgresql@<version>` on macOS).

**Two clones of this repo sharing data** — `docker-compose.yml` sets the project name explicitly (`name: lq-ai`), so Compose does not derive it from the checkout folder: two clones will reuse each other's containers and named volumes (`lq-ai_pgdata` etc.) whatever their folders are called — including the database, admin user, and object-store data — and `docker compose down` in one will tear down the shared stack. To isolate two parallel clones, set `COMPOSE_PROJECT_NAME` in each clone's `.env` to a unique name (e.g. `COMPOSE_PROJECT_NAME=lq-ai-dev` and `COMPOSE_PROJECT_NAME=lq-ai-prod-candidate`), or pass `-p <name>` on every command; both override the file's `name:`. Give each clone its own host ports too if they run at the same time.

---

### Where your data lives

Everything the stack writes lands in named Compose volumes — none of them live inside a container, so `docker compose down` on its own leaves them intact; only `-v` removes them, and this repository's own dev-environment rules flag `down -v` as the thing never to run without a backup first. For what each volume holds, why `miniodata` keeps its name after the RustFS swap, and how to back it up, see [Backup and restore](/lq-ai/operate/backup-and-restore/).

---

### How to verify what this project says about itself

The trust model for a self-hosted, open-source project is that every claim terminates in code you can read. Here are the five most important verification paths:

- **E2E test suite:** `web/cypress/e2e/` — every M1 user-facing flow has a Cypress spec. Read the test to understand what the claim covers.
- **Inference Gateway:** `gateway/app/router.py` — the only egress point, the security boundary, the place where routing decisions are made and logged.
- **Audit log writer:** `api/app/audit.py` — what gets written to the `audit_log` table, and for which actions.
- **Honest catalog:** [`docs/HONEST-STATE.md`](/lq-ai/trust/published-gaps/) — the shipped-vs-deferred table with a verification path for every row. If you find a discrepancy between this document and the code, the code is canonical.
- **Interactive architecture tour:** Navigate to `http://localhost:3000/lq-ai/learn` after logging in. Each playground points at the relevant source files for that topic.

---

### First steps after login

**The Learn page is the guided tour.** New users and procurement evaluators start at `http://localhost:3000/lq-ai/learn`. Its interactive playgrounds walk through the architecture, the full request lifecycle, the five-tier inference model, what the model actually sees (and what it doesn't), where data lives, and how to author your own skill. Each playground links to the relevant source file.

**The honest catalog.** [`docs/HONEST-STATE.md`](/lq-ai/trust/published-gaps/) names what is shipped, what is deferred, and how to verify each. We publish this because the verification path for an open-source product terminates in code, not in claims. If anything in this README is inconsistent with what the code does, please [open an issue](https://github.com/LegalQuants/lq-ai/issues) — the code is canonical.

**Want to contribute?** [`docs/ROADMAP.md`](https://github.com/LegalQuants/lq-ai/blob/8c66c85a/docs/ROADMAP.md) is the live, ordered punch list of work that has not yet shipped — distilled from PRD §8/§9, HONEST-STATE, and the active milestone plans into one prioritized contributor view, labelled by complexity (🟢/🟡/🔴), effort (S/M/L), and contributor profile (engineer / attorney / security / DevOps). For the curated short-cycle subset where the foundation is already in source and the gap is written down in advance, see [`docs/contribute/EASIEST-CONTRIBUTIONS.md`](/lq-ai/contribute/board/) — currently seven items, ranging from "a practicing attorney with no engineering background can pick this up" to "a security architect familiar with OWASP can pick this up." Each mini-PRD names the acceptance criteria, the contributor profile, and the files to start in.

---

## Next

- [Give the app a secure web address](/lq-ai/operate/reverse-proxy-tls/)
- [Use AI without an internet connection](/lq-ai/operate/air-gapped/)
- [How the parts fit together](/lq-ai/operate/architecture/)
