Skip to content

Install with Docker Compose

How-to
  • operator

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+ (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.

Terminal window
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 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:

Terminal window
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 before starting the new stack.

Terminal window
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:

Terminal window
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.

Terminal window
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.

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

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.

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. Google Vertex AI remains on the deferred-enhancement list (DE-034) and welcomes community contribution — see docs/HONEST-STATE.md 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.

Section titled “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). To turn on case-law research:

  1. In .env, set COURTLISTENER_API_TOKEN to a token from 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; 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.

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:

Terminal window
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.


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.


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.


How to verify what this project says about itself

Section titled “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 — 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.

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 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 — the code is canonical.

Want to contribute? 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 — 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.