Install with Docker Compose
Quick Start
Section titled “Quick Start”In short
Section titled “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+ (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
Section titled “Step 1 — Clone and configure”git clone --recurse-submodules https://github.com/LegalQuants/lq-ai.gitcd lq-aicp .env.example .env
--recurse-submodulespulls in the LegalQuants/lq-skills community skills catalog (30+ skills authored by lawyer-builders across 17+ jurisdictions). If you forgot the flag, rungit submodule update --init --recursivefrom inside the cloned repo.
Open .env and set the four required secrets (the file explains each one):
POSTGRES_PASSWORD— any long random stringOBJECT_STORE_SECRET_KEY— any long random stringLQ_AI_GATEWAY_KEY— any long random stringJWT_SECRET— any long random string
Generate all four at once with Python 3 (python3), labelled and ready to paste into .env:
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.
Step 2 — Start the stack
Section titled “Step 2 — Start the stack”docker compose up -dEight 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:
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 apiforFirst-run admin passwordto retrieve it. Most operators ignore this and just run Step 3 below, which sets a known password directly.
Step 3 — Set the admin password
Section titled “Step 3 — Set the admin password”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-changeThis 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 execreports the container isn’t found, rundocker compose psto see the actual container name on your setup — some Docker Compose versions uselq-ai_api_1(underscores) instead oflq-ai-api-1(hyphens). Thedocker compose exec api python -m app.cli ...form also works on most setups if you prefer it.
Step 4 — Log in
Section titled “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-aiBackend API docs: http://localhost:8000/docs (Swagger UI)Backend API docs: http://localhost:8000/redoc (ReDoc)Inference Gateway: http://localhost:8001/docsStep 5 — First 5 minutes
Section titled “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.mddriving it. - Click + New matter to create your first project workspace and attach a document to try a skill against.
Providers and air-gapped deployments
Section titled “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. 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.
Enabling legal-research connectors (case-law / MCP) — operator opt-in
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:
- In
.env, setCOURTLISTENER_API_TOKENto a token from courtlistener.com/help/api (thedocker-compose.ymlgateway service forwards this variable into the gateway container). - In
gateway.yaml, uncomment thetool_providers:→courtlistener-prodblock (the template is ingateway.yaml.example;api_key_env: COURTLISTENER_API_TOKEN). - Recreate the gateway so it picks up the new
.envvalue:docker compose up -d --force-recreate gateway(a plaindocker compose restartkeeps the container’s old environment). Confirm as a signed-in user — the capabilities route requires a bearer token (fromPOST /api/v1/auth/login):curl -s -H "Authorization: Bearer $TOKEN" localhost:8000/api/v1/research/capabilities→"enabled": true. An unauthenticated call returns401, 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:
docker compose --profile local up -dIn 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
Section titled “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 neededWEB_HOST_PORT=3000 # change if another web app (Next.js dev, etc.) holds :3000API_HOST_PORT=8000 # change if a host service (Django, FastAPI) holds :8000GATEWAY_HOST_PORT=8001 # change if another service holds :8001REDIS_HOST_PORT=6379 # change if a host redis holds :6379OBJECT_STORE_API_HOST_PORT=9000 # change if another service holds :9000OBJECT_STORE_CONSOLE_HOST_PORT=9001 # change if Portainer/Console holds :9001The 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
Section titled “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.
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 theaudit_logtable, 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/learnafter logging in. Each playground points at the relevant source files for that topic.
First steps after login
Section titled “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 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.