Skip to main content
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/, 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.11, frontend on Node 20), so no host Python or Node is required.
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

2

Create the env files

deploy.sh copies them from the examples on first run, or do it yourself:
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

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, 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, which starts four services: On first start the backend’s 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):
  • 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): the default LLM provider/model, CORS origins, and observability.provider (none to disable Phoenix/LangSmith tracing). Secrets never come from YAML.
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.
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

Resetting

For how this same stack is built and shipped to the dev and production servers, see Deployment.