LazyDev is an AI-native autonomous CI engineering assistant that monitors GitHub issues, generates validated code fixes, and pushes fix branches safely.
This is the self-hosted build of the backend: you run the entire stack yourself. There is no hosted/SaaS mode in this repo — multi-tenant GitHub OAuth, the isolated MCP port, and the hosted control plane have been stripped out.
- Prerequisites
- Create and configure your GitHub App
- Environment setup
- LLM & embedding providers (Backend & Client options)
- Run the full stack (Docker) & optional webhook tunnel
- Local development
- MCP server (Hermes / OpenClaw / Claude Desktop)
- Notifications without an AI agent (Discord / Slack / Telegram)
- Observability
- Connecting the frontend client dashboard
- Development standards
- Features
- License
- Cloud server / VM: Linux (Ubuntu/Debian recommended) for production, or macOS/Linux for local development.
- Node.js v20+ (only needed for local dev; the Docker image bundles Node).
- Docker & Docker Compose.
- GitHub App credentials — App ID, Private Key (
.pem), Webhook Secret (created by you; see §2). - LLM API key — OpenAI / Google Gemini / Anthropic / DeepSeek / OpenRouter, or a running Ollama instance (free local fallback).
- Discord webhook URL (optional, for push notifications without an AI agent — see §8; Slack/Telegram via gateway bridge).
LazyDev interacts with GitHub through a GitHub App registered under your account or organization. Since this is a self-hosted installation, you create and manage your own GitHub App for complete privacy and control over your repositories.
-
Go to GitHub: Settings → Developer settings → GitHub Apps → New GitHub App.
-
Name: Choose a unique name (e.g.
lazydev-<your-team>). -
Homepage URL: Set to your repository or company domain (e.g.
https://github.com/<your-org>/lazydev-server). -
Webhook URL: Set to
https://<your-public-url>/webhooks/github(see §5 for setting up a public domain or running an ngrok tunnel). -
Webhook secret: Enter a strong secret string (you will put this into
.envasGITHUB_WEBHOOK_SECRET). -
Under Repository permissions, grant:
Permission Level Why Metadata Read Mandatory for all GitHub Apps. Contents Read & write Clone repository code and push fix branches. Pull requests Read & write Open and update pull requests with validated fixes. Issues Read & write Read issues; write is required by the MCP implement_new_featuretool to create tracking issues. -
Under Subscribe to events, select:
- Issues
- Issue comment
- Check run
-
Click Create GitHub App.
-
In the newly created app's settings:
- Copy the App ID (save this for
GITHUB_APP_IDin.env). - Scroll to Private keys and click Generate a private key. A
.pemfile will download. Move this file into yourlazydev-serverdirectory (e.g.github-private-key.pem).
- Copy the App ID (save this for
-
In the left sidebar, click Install App, choose your account or organization, and select Only select repositories (the repos you want LazyDev to monitor) or all repositories.
Important
If you update repository permissions later, GitHub emails the installation owner an approval request. The app retains its old permissions until approved, so operations requiring new permissions will fail until accepted.
Clone this repository and copy the example environment file:
git clone https://github.com/FutureMindsDev/lazydev-server.git
cd lazydev-server
cp .env.example .envEdit .env. The file is grouped into clearly labelled sections (GitHub App, LLM provider, Embedding, Serena, MCP server, Sandbox, Infrastructure) and each variable is tagged [DOCKER], [DEV], or BOTH. The minimum you must fill in:
# GitHub App (from §2)
GITHUB_APP_ID=123456
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
GITHUB_PRIVATE_KEY_PATH=github-private-key.pem
# LLM provider — RECOMMENDED: set LLM_CONFIG_ENCRYPTION_KEY (.env §2a)
# and add your provider in the dashboard (Settings → LLM provider).
# Headless alternative (see §4): fill in the §2b env keys instead.
# OPENAI_API_KEY=sk-...
# LLM_MODEL=gpt-4o-miniEverything else has sensible defaults for a Docker deployment.
Do I need the DB / Redis / server / repo-cache / sandbox configs in
.env?
No, if you are running the full stack viadocker compose up. Thedocker-compose.ymlalready setsDB_HOST=postgres,REDIS_HOST=redis,QDRANT_URL=http://qdrant:6333,WORKTREE_BASE_PATH=/app/worktrees, etc. Those variables are listed in.env.exampleonly so you can override them for local development (npm run start:dev) or external infrastructure.
LazyDev supports a wide range of LLM and embedding providers with smart auto-detection. .env.example numbers the LLM setup as §2a → §2b → §2c on purpose:
| Section | What | When |
|---|---|---|
| §2a — dashboard UI (RECOMMENDED) | Set LLM_CONFIG_ENCRYPTION_KEY, then add providers in Settings → LLM Provider |
Normal use. No secrets in files, keys rotatable from the UI. |
§2b — .env provider |
OPENAI_API_KEY + OPENAI_BASE_URL + LLM_MODEL (Options A–L below) |
Headless servers / CI / no frontend. Ignored when a §2a row exists for the run. |
§2c — per-agent .env overrides |
PLANNER_MODEL, PATCH_GENERATOR_API_KEY, … |
Route one agent to a different provider/model. Replaced by §2a when a dashboard row exists (use the UI's per-agent role mapping instead). |
Start with §2a. Only fill in §2b if you are deliberately not using the dashboard.
You can configure credentials using either of two methods:
- Option 1 — Configure keys via the client dashboard (
lazydev-client) [§2a, RECOMMENDED]: Dynamic UI configuration stored securely in PostgreSQL without restarting containers or editing files. - Option 2 — Configure keys via the backend (
.env) [§2b/§2c]: Ideal for headless server deployments, Docker configurations, and automated CI/CD.
LazyDev auto-detects the provider from OPENAI_BASE_URL and routes to the right client: direct Gemini, DeepSeek, and Anthropic go through their provider-native LangChain clients (which handle their payload-specific requirements natively — e.g. Gemini 3 thought_signature round-trips), while OpenAI, OpenRouter, Ollama, and any other OpenAI-compatible gateway (LiteLLM, vLLM) go through @langchain/openai's Chat Completions client.
Set OPENAI_API_KEY + OPENAI_BASE_URL + LLM_MODEL:
| Provider | OPENAI_BASE_URL |
Example LLM_MODEL |
Client |
|---|---|---|---|
| OpenAI (native) | (leave blank) | gpt-4o-mini |
ChatOpenAI |
| Google Gemini (direct) | https://generativelanguage.googleapis.com |
gemini-3.7-flash |
ChatGoogleGenerativeAI (native) |
| OpenRouter | https://openrouter.ai/api/v1 |
anthropic/claude-5-sonnet |
ChatOpenAI (OpenAI-compat) |
| DeepSeek (direct) | https://api.deepseek.com |
deepseek-chat |
ChatDeepSeek (native) |
| Anthropic (direct) | https://api.anthropic.com |
claude-sonnet-4-5 |
ChatAnthropic (native Messages API) |
| NVIDIA NIM | https://integrate.api.nvidia.com/v1 |
meta/llama-3.1-405b-instruct |
ChatOpenAI (OpenAI-compat) |
| Z.AI / GLM | https://api.z.ai/api/paas/v4 |
glm-4.6 |
ChatOpenAI (OpenAI-compat) |
| MiniMax | https://api.minimaxi.com/v1 |
MiniMax-M2 |
ChatOpenAI (OpenAI-compat, stripThinkTokens guard) |
| Xiaomi MiMo | https://api.xiaomi.com/v1 |
mimo-7b |
ChatOpenAI (OpenAI-compat) |
| Kimi (Moonshot) | https://api.moonshot.cn/v1 |
kimi-k2 |
ChatOpenAI (OpenAI-compat) |
| Grok (xAI) | https://api.x.ai/v1 |
grok-4 |
ChatOpenAI (OpenAI-compat) |
| Ollama (local, free) | (leave OPENAI_API_KEY blank) |
llama3 (set OLLAMA_LLM_MODEL=llama3) |
ChatOpenAI (Ollama's OpenAI-compat endpoint) |
MiniMax / interleaved-thinking models: some models return
<think>…</think>reasoning blocks insideresponse.content. LazyDev strips these automatically (stripThinkTokensguard) before downstream text processing.
Every agent shares the §2b OPENAI_API_KEY / OPENAI_BASE_URL / LLM_MODEL by default. You can override any agent to use a different provider (its own API key + base URL) and/or a different model. (Note: when a §2a dashboard row exists for the run, it fully replaces env — including these overrides. Use the dashboard's per-agent role mapping instead.)
# PlannerAgent — strong reasoning model on the shared provider
PLANNER_MODEL=gpt-4o
# PatchGeneratorAgent — strong coding model on the shared provider
PATCH_GENERATOR_MODEL=gpt-4o
# OnboardingAgent — cheap DeepSeek (full provider + model override)
ONBOARDING_MODEL=deepseek-chat
ONBOARDING_API_KEY=sk-deepseek-...
ONBOARDING_BASE_URL=https://api.deepseek.com
# ValidationAgent — fast & economical model on the shared provider
VALIDATION_MODEL=gpt-4o-mini
# GitAgent — git commands only, no LLM neededEvery agent supports the <AGENT>_MODEL, <AGENT>_API_KEY, <AGENT>_BASE_URL trio for PLANNER, PATCH_GENERATOR, VALIDATION, GIT, and ONBOARDING. Leaving any line commented falls back to the shared defaults.
EMBEDDING_PROVIDER selects the embedding backend (google | openai | ollama; auto-detected if unset). EMBEDDING_MODEL is the model name:
| Provider | Models |
|---|---|
gemini-embedding-2 (default), text-embedding-004, text-multilingual-embedding-002 |
|
| OpenAI | text-embedding-3-small, text-embedding-3-large |
| Ollama | nomic-embed-text, mxbai-embed-large |
By default, the embedding service reuses OPENAI_API_KEY / OPENAI_BASE_URL (or GEMINI_API_KEY). If your embedding provider differs from your LLM provider, set decoupled overrides:
EMBEDDING_API_KEY=sk-...
EMBEDDING_BASE_URL=https://api.openai.com/v1Instead of managing API keys inside .env files, you can configure and update your LLM providers directly from the web UI using the companion frontend repository lazydev-client.
- Start
lazydev-client(see §10) and openhttp://localhost:3000. - Navigate to Settings → LLM Provider.
- You have two flexible ways to manage keys inside the UI:
- Default BYOK Provider: Choose your provider (OpenAI, Gemini, Anthropic, DeepSeek, OpenRouter, Ollama, etc.), enter your API key, and select or type the default model name.
- Per-Agent Provider Role Mapping: Add multiple named provider configs and assign specific agent roles (Planner, Patch Generator, Validation, Onboarding) to different providers and models. For instance, you can route the Planner and Patch Generator to Claude Sonnet 3.5 while routing Onboarding to a lightweight DeepSeek or Gemini Flash model.
- The client saves credentials securely to the backend via
PUT /api/dashboard/settings/llmandPOST /api/dashboard/settings/providers. - Secrets are encrypted at rest with AES-256-GCM in PostgreSQL (
llm_configandllm_provider_configtables). - To enable production encryption, set
LLM_CONFIG_ENCRYPTION_KEY(32-byte hex, generated withopenssl rand -hex 32) in.env. If left unset in dev mode, a deterministic dev key is used. - API keys are write-only — the UI masks them and only ever displays the last 4 characters.
Matching the pipeline code (LlmService.getModel — a resolved dashboard row fully replaces env for that run, including per-role env overrides):
- Dashboard per-agent mapping (§2a UI: agent → provider assignment / model override) — most specific.
- Dashboard shared row (§2a UI: default provider + model) — replaces all
.envLLM values for the run. - Per-agent
.envoverrides (§2c:PLANNER_MODEL, etc.) — only when no dashboard row exists. - Shared
.envdefaults (§2b:OPENAI_API_KEY,LLM_MODEL). Deleting the dashboard config immediately reverts to.envresolution on the next run.
Launch the entire LazyDev infrastructure with Docker Compose:
docker compose up -d --buildUse
--buildthe first time and whenever you change source code or theDockerfile. For config-only changes (.env,docker-compose.yml), restart without rebuilding:docker compose up -d
The app container requires access to /var/run/docker.sock so the SandboxAgent can spin up sibling containers for isolated code execution and validation.
Warning
Mounting /var/run/docker.sock gives the container control over the host Docker daemon. This is strictly required for the SandboxAgent. The container runs as root (user: root in docker-compose.yml) to ensure socket access. Ensure your host machine is adequately secured.
LazyDev uses a named Docker volume (worktrees) to share workspace files between the app container and sandbox sibling containers. This works identically across platforms without host path binding issues.
LazyDev requires a public HTTP endpoint (POST /webhooks/github) to receive event webhooks (issues.opened, issue_comment.created, etc.) from GitHub. Each request's HMAC-SHA256 signature is verified against your GITHUB_WEBHOOK_SECRET before dispatching jobs to BullMQ.
You have two mutually exclusive paths — pick one, don't mix them. Full variable reference: .env.example §10.
The tunnel forwards to the app service on port 3200, so you don't install ngrok/tailscale on your host.
- Sign up at ngrok.com and get an authtoken.
- Add your token to
.env(leave it blank if you use Path B):NGROK_AUTHTOKEN=your_ngrok_authtoken_here # NGROK_DOMAIN=your-reserved-domain.ngrok-free.app # Optional (paid plan)
- Open
docker-compose.ymland uncomment thengrok:service block (located under the# Option 1: ngrokheader — it ships commented out in this repo). - Start the tunnel container using the tunnel profile:
(Plain
docker compose --profile tunnel up -d
docker compose up -dskips the tunnel — local-only testing.) - View the public URL printed in the ngrok container logs:
(Look for the line containing
docker compose logs ngrok
url=https://<random-id>.ngrok-free.app). - Set your GitHub App's Webhook URL (in your app's General settings) to:
https://<random-id>.ngrok-free.app/webhooks/github - Tip: You can view live incoming webhook payloads using ngrok's web inspection dashboard at
http://localhost:4040.
Free tier gives a random URL each restart — reserve a domain (paid plan) via NGROK_DOMAIN for a stable URL.
- Get an auth key from Tailscale Admin.
- Set
TAILSCALE_AUTH_KEY=...in.env. - Uncomment the
tailscale:service block indocker-compose.yml. - Start with:
docker compose --profile tunnel up -d. - Enable HTTPS + Funnel in the Tailscale admin console and set your Webhook URL to:
https://<machine>.<tailnet>.ts.net/webhooks/github.
Use this when you already have a public server or run ngrok on the host — Docker must not create a second tunnel:
- Leave
NGROK_AUTHTOKENblank and start without the tunnel profile:docker compose up -d(keep thengrok:/tailscale:blocks commented out so they can never start by accident). - Point your existing public endpoint at the app's port 3200:
- Reverse proxy with SSL (recommended for production): Caddy / nginx / Traefik with a valid certificate →
https://lazydev.your-domain.com/webhooks/github. - Host-level ngrok:
https://<host-ngrok-url>/webhooks/github. - Direct public IP (quick & dirty):
http://<your-server-ip>:3200/webhooks/github(GitHub allows plain HTTP, but payloads travel unencrypted).
- Reverse proxy with SSL (recommended for production): Caddy / nginx / Traefik with a valid certificate →
Rule of thumb: Path A = "Docker gives me the public URL." Path B = "I already have a public URL; Docker stays out of the way."
Run infrastructure in Docker, but run the NestJS app directly on your host machine for hot reloading:
docker compose up -d postgres redis qdrant
npm install
npm run start:devFor local development, set the infrastructure connection variables in .env to localhost (DB_HOST=localhost, REDIS_HOST=localhost).
LazyDev exposes itself as an MCP server over Streamable HTTP, allowing MCP-capable platforms (Hermes, OpenClaw, Claude Desktop) to trigger and supervise fixes directly from chat. Tools are discovered automatically during the MCP protocol handshake.
| Tool | Purpose |
|---|---|
trigger_issue_fix(repository, issue_number, priority?) |
Fix an existing GitHub issue. priority: "urgent" jumps the queue. |
implement_new_feature(repository, feature_description) |
Write net-new code from a prompt. Creates a tracking issue (labelled enhancement, lazydev). |
get_pipeline_status(task_id) |
Check whether a run is queued, running, failed, or succeeded. |
provide_human_feedback(task_id, feedback) |
Send corrections. Active tasks apply feedback on the next validation retry; already-failed tasks are re-run with it applied. |
list_my_repositories() |
List every repo LazyDev has onboarded and you can access. |
get_repository_health(repository) |
Composite health summary: cache/index status, Qdrant vector stats, recent runs + success rate, in-flight tasks. |
Write tools return a task_id immediately — jobs run asynchronously on the BullMQ queue.
Hermes is the conversational remote control for LazyDev — instead of opening the dashboard, you chat from your laptop (or phone via the WhatsApp bridge) and Hermes calls the tools above for you:
- Check status without the dashboard — "is my fix running?" → Hermes calls
get_pipeline_status(task_id); "which repos are wired up?" →list_my_repositories(); "is repo X healthy?" →get_repository_health("owner/repo")(index stats, recent success rate, in-flight jobs). - Steer the patch mid-run — read the PR diff, then tell Hermes "use a guard clause instead of nested ifs" → it calls
provide_human_feedback(task_id, feedback)and the agent applies it on its next validation retry. Failed tasks are automatically re-queued with your note attached. - Trigger work in plain language — "fix issue 42 in owner/repo, it's urgent" →
trigger_issue_fix; "add dark mode to the settings page" →implement_new_feature(creates a tracking issue so the PR stays traceable). - No webhooks to babysit — Hermes polls LazyDev over MCP, so unlike Discord/Slack (§8) you don't configure any webhook URL; if your laptop can reach
http://<host>:3200/mcp, you get live answers.
Typical loop: Hermes trigger_issue_fix → you get a task_id → Hermes
get_pipeline_status while you wait → you review the PR → Hermes
provide_human_feedback to iterate. §8 covers the alternative: push-only
webhook notifications for people who don't run an AI agent at all.
The MCP surface is served on the main API port at POST http://<host>:3200/mcp (shared with the REST API and GitHub webhooks).
Set MCP_SERVER_ENABLED=false to turn the MCP surface off completely.
Warning
Since this endpoint shares port 3200, set MCP_AUTH_TOKEN in .env to require Authorization: Bearer <token>. Without it, anyone with network access to port 3200 can trigger pipeline jobs.
Add LazyDev to your MCP client config (e.g. claude_desktop_config.json):
{
"mcpServers": {
"lazydev": {
"url": "http://localhost:3200/mcp",
"transport": "streamable-http",
"headers": { "Authorization": "Bearer YOUR_MCP_AUTH_TOKEN" }
}
}
}Verify with the smoke script:
npx ts-node test/test-mcp-server.ts # default
MCP_AUTH_TOKEN=s3cret npx ts-node test/test-mcp-server.ts # with authNo Hermes, no OpenClaw, no chat agent? You can still get push alerts for pipeline events (CI pass/fail on fix branches, job updates). This path is one-way webhooks — the server messages you; you reply in GitHub, not in chat. (For two-way steering from chat, use the MCP tools in §7 instead.)
- Discord channel → Edit Channel → Integrations → Webhooks → New Webhook.
- Copy the webhook URL.
- Paste it into
.env(see.env.example§5):DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
- Restart the app (
docker compose up -d). The next CI result on alazydev/fix-*branch arrives as a rich embed.
Only DISCORD_WEBHOOK_URL is read natively today. To reach other chats,
forward the payload through a gateway — pick one:
- Slack Incoming Webhook + bridge: create a Slack Incoming Webhook, then add a tiny bridge (n8n / Make / Zapier / a ~20-line Cloudflare Worker) that reshapes the Discord embed JSON into Slack blocks.
- n8n / Make fan-out (recommended for teams): one HTTP-webhook scenario
receives the event and forwards to Discord and Slack and Telegram
at once. Set the scenario's URL as
GENERIC_NOTIFICATION_WEBHOOK_URLin.env(reserved placeholder,.env.example§5) so the wiring is documented even before native support lands. - Telegram via gateway: create a bot with BotFather, get the chat ID,
and let the same n8n/Make scenario deliver via the Telegram Bot API
(
TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_IDplaceholders in.env.example§5 are reserved for this). - WhatsApp via Hermes: run the Hermes WhatsApp bridge on your laptop and ask it for status — no webhook URL needed (see §7 "Why run Hermes on your laptop").
Leave
DISCORD_WEBHOOK_URLblank to silence notifications entirely — the server logs a warning and continues.
The app exposes /metrics (Prometheus format) via @willsoto/nestjs-prometheus. docker-compose.yml provides optional Prometheus and Grafana containers.
| Dashboard | URL | Default login |
|---|---|---|
| NestJS API | http://localhost:3200 | — |
| pgAdmin (optional) | http://localhost:5050 | admin@lazydev.com / admin |
| RedisInsight (optional) | http://localhost:8001 | — |
| Grafana (optional) | http://localhost:3100 | admin / admin |
| Prometheus (optional) | http://localhost:9090 | — |
The dashboard UI lives in the lazydev-client repository — a Next.js 16 application that interfaces with this backend's /api/dashboard/* endpoints.
Verify the following CORS and auth settings in lazydev-server/.env:
DEPLOYMENT_MODE=selfhosted
DASHBOARD_AUTH=none
# CORS: list all origins the frontend is served from
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
FRONTEND_URL=http://localhost:3000Start the backend:
docker compose up -d --build # API running on http://localhost:3200Clone lazydev-client next to this repo:
git clone https://github.com/FutureMindsDev/lazydev-client.git
cd lazydev-client
pnpm install
cp .env.example .env.localEdit .env.local:
# Point at the LazyDev backend API
NEXT_PUBLIC_API_URL=http://localhost:3200
# Connect to real backend
NEXT_PUBLIC_ENABLE_MOCKS=falseStart the dashboard:
pnpm dev # http://localhost:3000Open http://localhost:3000 to view real-time pipeline runs, queue states, repository indexing, and dynamic LLM provider settings. See the client repo's BACKEND_API_SPEC.md for full API contracts.
- Linting:
npm run lint - Formatting:
npm run format - Tests:
npm run test - Commits: Conventional Commits enforced via Husky.
- GitHub App Authentication: JWT + Installation Token auth.
- Secure Webhooks: HMAC-SHA256 signature verification with delivery-ID deduplication.
- Asynchronous Ingestion: BullMQ + Redis queue with exponential backoff retries.
- Distributed Locking: Redis Redlock prevents concurrent execution anomalies per repo and branch.
- Multi-Agent Orchestration: LangGraph AI pipeline — Onboarding → Planner → PatchGenerator → ValidationAgent → GitAgent.
- Dynamic Provider Configuration: Configure LLM providers via backend
.envor client dashboard UI with AES-256-GCM encryption. - Per-Agent Model Selection: Assign custom providers and models to individual agents.
- Sandbox Validation: Fixes verified with
npm run buildinside isolated Docker containers. - Cross-Platform Sandbox: Named Docker volume for worktrees ensures cross-platform reliability.
- Self-Healing Loop: Automatically retries patch generation incorporating validation feedback.
- MCP Server (Hermes / OpenClaw): Exposes 6 orchestration tools over Streamable HTTP (trigger fix, new feature, pipeline status, human feedback, repo list, repo health).
- Notifications: Discord webhook integration for pipeline status alerts (Slack/Telegram via gateway bridge — no AI agent required).
MIT — see LICENSE.