Set up for development
LQ.AI is three services with no shared in-process code — api/ (FastAPI), gateway/ (the
Inference Gateway), and web/ (a SvelteKit fork of OpenWebUI) — talking over HTTP through
OpenAPI contracts. This page is the practical loop for running all three, plus the
database, worker, and object-store services they depend on.
In short
Section titled “In short”You don’t need the whole stack to read code or run the Python or Gateway test suites —
but trying the app in a browser needs the website, API, and their supporting services
together. docker compose up -d (or docker compose --profile local up -d for local Ollama, no
external key) brings up all of it in one command; make run-dev/make stop-dev wrap the same bring-up
and shutdown. Python dependencies for api/ and gateway/ are locked with uv
(make install-api/make install-gateway); only relock uv.lock when deliberately
changing dependencies. The dockerized web container serves a prebuilt bundle, not a live
dev server, so editing frontend code needs Vite running natively on the host alongside it
— see the exact values further down. Two things corrupt a shared dev stack: running
Alembic migrations from the host against the live database, and docker compose down -v.
Bring up the stack
Section titled “Bring up the stack”You need Git (git) and Docker with Compose:
git clone https://github.com/legalquants/lq-ai.gitcd lq-aicp .env.example .env# Edit .env with at least one LLM provider API key — or use the local profiledocker compose up -d # Mode 1: bring-your-own-key providers# ORdocker compose --profile local up -d # Mode 2: local Ollama, no external keyThis starts postgres, redis, rustfs, gateway, api, ingest-worker, arq-worker,
and web; the local profile adds ollama (docker-compose.yml). rustfs is the object
store — it replaced a bundled MinIO (ADR 0036); the
OBJECT_STORE_* environment variables are the current contract, with the old MINIO_*
names still accepted as a fallback. make run-dev wraps the same bring-up command; make stop-dev stops the stack without touching volumes.
Python dependencies: api/ and gateway/
Section titled “Python dependencies: api/ and gateway/”Both subsystems lock dependencies with uv, decided in ADR 0023 so the transitive dependency tree is deterministic and dependency pull requests at the Gateway’s security boundary carry concrete versions in the diff. For ordinary setup:
make install-api # or install-gatewaycd api && .venv/bin/uvicorn app.main:app --reload --port 8000uv sync --frozen --extra dev creates .venv/ from the committed uv.lock without
changing it. Only run uv lock in a subsystem when you are deliberately changing
pyproject.toml, and commit the updated lockfile — CI gates on uv lock --check, so a
pyproject.toml edit without a relocked uv.lock fails CI, not only review.
Run the Python test suite in Docker if you don’t want a local venv:
docker build -f api/Dockerfile.dev -t lq-ai-api-dev api/docker run --rm lq-ai-api-dev python -m pytest tests/ -xDockerfile.dev installs the [dev] extras (pytest, respx, pytest-cov) on top of the
production image; the production Dockerfile deliberately does not, to keep the runtime
image’s surface area minimal.
Web: dependencies and the HMR gap
Section titled “Web: dependencies and the HMR gap”make install-web # installs the committed package-lock.jsoncd web && npm run devweb’s Docker image, though, only builds a static SvelteKit bundle — there is no source
bind-mount and no dev server wired into the compose service. Editing web/src and
refreshing the browser shows nothing until you rebuild the image, which is a multi-minute
loop per change. web/docs/frontend-dev.md documents the
fix: run Vite natively on the host for hot-module reload, while keeping the dockerized
web container running (republished on port 8080) because OpenWebUI’s own embedded
backend, which the SPA depends on at boot, only ships inside that image. The one-time
setup adds Vite’s origin to LQ_AI_CORS_ORIGINS and points web/.env at the api
container; day to day, it’s WEB_HOST_PORT=8080 docker compose up -d … followed by npm run dev, then browsing to :5173 rather than :8080 or :3000. Before shipping a
frontend change, build the production image at least once and click through it there —
Vite dev and the static-adapter build can diverge.
Concretely, the one-time setup is two values. In the root .env,
LQ_AI_CORS_ORIGINS=http://localhost:3000,http://localhost:5173, then recreate api
(docker compose up -d api) so the container picks up the new value. In web/.env
(create the file if needed), PUBLIC_LQ_AI_API_BASE_URL=http://localhost:8000/api/v1,
because Vite only reads env files from web/, never the repository root. Each has its
own failure mode: without the web container republished on :8080, npm run dev
starts but the SPA redirects to its “Backend Required” error page, because dev mode
hard-codes the embedded backend at <hostname>:8080; without web/.env, the client
falls back to a relative /api/v1, which resolves against :5173 and 404s. Two things
the Vite loop does not cover: a change under web/backend/ (OpenWebUI’s embedded
backend, not LQ.AI’s api/) still needs docker compose build web, and npm run dev
fetches Pyodide artifacts into web/static/pyodide before starting — a one-time cost
per checkout, not per restart. To return to the fully dockerized stack, stop Vite and
run docker compose up -d web without the override; .env’s default
WEB_HOST_PORT=3000 takes over.
When deliberately changing a Web dependency, use npm install <package> (or npm uninstall) and commit both web/package.json and web/package-lock.json; use npm ci
for ordinary setup.
Migrations
Section titled “Migrations”make migrate # run Alembic migrations against the running api containermake migrate-status # show the current headWhen a migration lands, rebuild api, arq-worker, and ingest-worker together. All
three are built from the api image and pin the same revision; rebuilding one alone
leaves the others crash-looping on a revision mismatch after the next restart.
Two more rules that corrupt a shared stack
Section titled “Two more rules that corrupt a shared stack”Never docker compose down -v. It wipes named volumes, including Postgres data,
object-store data, and any acceptance data that is expensive to recreate. Rebuild a single
service instead: docker compose build web && docker compose up -d web.
Run both ruff format and ruff check before pushing — CI runs them as separate
gates, and a format pass does not imply a lint pass.
- On-ramp for engineers — the gates and PR process this environment feeds into
- Point a coding agent at the repository