Self-hosting
Quickstart

Run the full Metis stack on one machine with Docker Compose, connect it to GitHub, and get your first AI review. Plan on about 20 minutes, most of it spent creating the GitHub App.
Clone the repository
bashgit clone https://github.com/KacemMathlouthi/metis.git cd metisCreate your GitHub App
Metis acts through a GitHub App that you own. Follow the GitHub App guide. For a local setup, use
http://localhost:5173/auth/callbackas the callback URL. You can fill in the webhook URL later, once your tunnel is running (step 6).When you're done, you will have:
- the App ID, Client ID, and a client secret
- the webhook secret you chose
- a private key
.pemfile
Add the private key
The Compose file mounts the key from a fixed path. Save your
.pemasbackend/metis-ai-testing.2025-11-21.private-key.pem, or change thevolumesentry for both theapiandworkerservices indocker-compose.dev.ymlto point at your file.The key is git-ignored (
*.pem). Never commit it.Configure the backend
Start from the example file:
bashcp backend/.env.example backend/.envGenerate the two application secrets:
bash# JWT_SECRET_KEY: signs session cookies openssl rand -hex 32 # ENCRYPTION_KEY: a Fernet key that encrypts GitHub tokens at rest openssl rand -base64 32 | tr '+/' '-_'Then edit
backend/.env. This is the minimum for a working setup. The full list is in the configuration reference.backend/.env# GitHub App (see the GitHub App guide) GITHUB_APP_ID=123456 GITHUB_CLIENT_ID=Iv23liExample GITHUB_CLIENT_SECRET_ID=your-client-secret GITHUB_WEBHOOK_SECRET=your-webhook-secret # LLM: any LiteLLM model string, plus that provider's key MODEL_NAME=gpt-4o OPENAI_API_KEY=sk-... # Secrets you generated above JWT_SECRET_KEY=... ENCRYPTION_KEY=... # Daytona sandboxes DAYTONA_API_KEY=dtn_... DAYTONA_API_URL=https://app.daytona.io/api DAYTONA_TARGET=us # Turn tracing off unless you have a LangSmith key LANGSMITH_TRACING=falseStart the stack
bashmake dev # same as: docker compose -f docker-compose.dev.yml up --buildThe API runs database migrations automatically on startup. When everything is healthy:
Service URL Dashboard http://localhost:5173API http://localhost:8000API reference (OpenAPI) http://localhost:8000/api/docspgAdmin http://localhost:5050Expose the webhook endpoint
GitHub needs to reach the API over the internet. Start a tunnel to port 8000:
bashngrok http 8000 # Forwarding https://f00d-1234.ngrok-free.app -> http://localhost:8000In your GitHub App settings, set the Webhook URL to the tunnel address followed by
/webhooks/github, for examplehttps://f00d-1234.ngrok-free.app/webhooks/github. GitHub sends apingevent. Check that the delivery succeeded under Advanced → Recent Deliveries.Sign in and enable a repository
- Install your GitHub App on the account or organization that owns your repositories, if you haven't already.
- Open
http://localhost:5173and click Login with GitHub. - Go to Repositories, click Sync from GitHub, then enable the repositories Metis should review.
Open a pull request
Open a PR on an enabled repository. Within a minute or two, inline findings start appearing on the diff, followed by a summary review and an updated PR description. Follow along with
make logs.
Troubleshooting
| Symptom | Likely cause |
|---|---|
Webhook deliveries fail with 401 Invalid Webhook Signature | GITHUB_WEBHOOK_SECRET doesn't match the secret in the GitHub App settings. |
Deliveries return ignored: “Repository not enrolled” | The repository is not enabled yet. Sync and enable it on the Repositories page. |
| GitHub shows “redirect_uri is not associated with this application” | The app's callback URL must be exactly <FRONTEND_URL>/auth/callback. |
| The API exits on startup with a validation error | A required variable is missing from backend/.env. The error names it. |
| Reviews start, then fail | Check the worker logs. The usual causes are an invalid LLM or Daytona key, or a MODEL_NAME that doesn't support tool calling. |
Running without Docker
For development on Metis itself, run the infrastructure in Docker and the app processes natively. You need Python 3.10+, uv, Node.js 20+, and pnpm.
make infra-up # PostgreSQL, Redis and pgAdmin only
make setup # uv sync (backend) + pnpm install (frontend)
make migrate # alembic upgrade head
# then, each in its own terminal:
make backend-dev # API on http://localhost:8000
make worker-dev # Celery worker
make frontend-dev # dashboard on http://localhost:5173In this mode, the values in backend/.env are used as-is. The defaults in .env.example already point at localhost. Set GITHUB_SECRET_KEY_PATH to your key's path relative to backend/.
Going to production
docker-compose.dev.yml is meant for development: it runs the dashboard dev server and uses default database passwords. For a long-lived deployment:
- Build the API and worker from
backend/Dockerfile. Both use the same image: the worker runscelery -A app.core.celery_app worker, and the API runsalembic upgrade headbeforeuvicorn. - Build the dashboard with the
productiontarget offrontend/Dockerfile, passingVITE_API_URLas a build argument. It serves static files with nginx on port 8080. - Use managed or properly secured PostgreSQL and Redis, with strong credentials.
- Serve the dashboard and API over HTTPS, on subdomains of the same domain (for example
app.example.comandapi.example.com). Session cookies areSameSite=Lax, so they are only sent when both share a site. - Set
FRONTEND_URLto the dashboard's public URL. It controls CORS and the OAuth redirect. Update the GitHub App's callback and webhook URLs to match.