Self-hosted
The same stack we run hosted: Postgres, the web app, and the worker. Free forever, no seat limit. Budget under 15 minutes from clone to a reviewed test PR.
Quickstart
git clone https://github.com/postil-dev/postil
cd postil
cp .env.example .env
# Fill in the required values before the first up. Each line in
# .env.example explains its variable. See "Required configuration"
# below for the full list and how to generate each one.
docker compose up -d
docker compose exec web bun run db:migrateThe compose file pins the reviewer CLI to a released version (POSTIL_CLI_REV, default a current tag) and fetches it during the image build, so a clean clone builds without extra steps. Set POSTIL_CLI_REV to a different tag or a 40-character commit to change the reviewer version, or drop a prebuilt binary at vendor/postil to skip the fetch.
Both web and worker validate their configuration at boot. A missing or malformed variable stops the process with the variable name, what it is for, and an example value — not a stack trace from the first request that happened to need it.
Required configuration
Compose injects DATABASE_URL for both services. Everything else comes from your .env. The web process refuses to boot without all of its required variables, and so does the worker.
Web
POSTIL_SESSION_SECRET: signs session cookies.openssl rand -hex 32.GITHUB_WEBHOOK_SECRET: verifies webhook signatures; must match the secret on the GitHub App.openssl rand -hex 32.GITHUB_OAUTH_CLIENT_IDandGITHUB_OAUTH_CLIENT_SECRET: dashboard sign-in. These come from a GitHub OAuth App, which is separate from the GitHub App (see below). The web container exits at boot if either is empty.POSTIL_SEALING_KEY: AES-256-GCM key sealing org BYO API keys at rest; required for both web and worker.openssl rand -hex 32.
Worker
GITHUB_APP_ID: numeric id from the GitHub App settings page.GITHUB_APP_PRIVATE_KEY: the App private key; raw PEM or base64-encoded PEM.POSTIL_SEALING_KEY: same key as web.- The LLM variables below are optional for boot but needed for reviews to run.
Pointing it at a model
These are worker variables. POSTIL_API_KEY falls back to OPENROUTER_API_KEY if it is unset. REVIEW_MODEL_CASCADE is an optional comma-separated list of fallback models tried in order on provider errors.
OpenRouter (default)
POSTIL_API_BASE=https://openrouter.ai/api/v1
POSTIL_API_KEY=sk-or-v1-...
REVIEW_MODEL=deepseek/deepseek-v4-pro
REVIEW_MODEL_CASCADE=qwen/qwen3-coderAzure OpenAI
POSTIL_API_BASE=https://<resource>.openai.azure.com/openai/v1
POSTIL_API_KEY=<azure-api-key>
REVIEW_MODEL=<deployment-name>Ollama (local, no API key)
Ollama is not part of the default stack; you run it yourself. The compose file ships an optional ollama service behind a profile; bring it up and pull a model before the first review:
docker compose --profile ollama up -d
docker compose exec ollama ollama pull qwen3-coder:30bThen point the worker at it on the compose network:
POSTIL_API_BASE=http://ollama:11434/v1
POSTIL_API_KEY=ollama # any non-empty value
REVIEW_MODEL=qwen3-coder:30bIf you already run Ollama on the host instead, drop the profile and use POSTIL_API_BASE=http://host.docker.internal:11434/v1 (add extra_hosts: ["host.docker.internal:host-gateway"] to the worker service on Linux).
The worker talks plain OpenAI-compatible chat completions, so anything that serves that API (vLLM, LiteLLM, TGI) works the same way.
postil doctor
Before opening a test PR, run the doctor inside the worker container. It resolves the config, checks the git work tree, the API key, a live probe of the model endpoint, and any forge tokens. Inside the worker it reads REVIEW_MODEL, POSTIL_API_BASE, and POSTIL_API_KEY from the container env, so set those in .env before running it:
docker compose exec worker postil doctor
[ok ] config loaded from defaults (model: qwen3-coder:30b, gate failOn: error, minConfidence: 0.6)
[FAIL] git not a git repository (local modes --staged/--base need one)
[ok ] api key POSTIL_API_KEY or OPENROUTER_API_KEY is set (value not shown)
[ok ] model endpoint http://ollama:11434/v1 answered for model qwen3-coder:30b
[ok ] forge tokens GITHUB_TOKEN unset, GITLAB_TOKEN unset (only needed for remote review)
postil doctor: ready.The git check reports FAIL inside the worker container because /app is not a work tree; that is expected and does not block PR reviews, which run against a fetched diff. The line that matters for setup is model endpoint: it must say your base answered for your model. Every failure prints the failing layer and a suggested fix. The documented anti-goal: a reviewer that silently falls back to a provider you did not configure.
GitHub setup
Self-hosting needs two distinct GitHub registrations: a GitHub App (delivers webhooks and mints installation tokens for reviews) and a GitHub OAuth App (dashboard sign-in). The web container will not boot without the OAuth credentials.
GitHub App
- Create a GitHub App on your org with permissions
contents: read,pull_requests: write,checks: write,metadata: read, and thepull_request,installation, andinstallation_repositoriesevents. For the interactive@postilbot, also addissues: write,issue_comment, and pull request review comment events. - Set the webhook URL to
https://your-host/api/webhooks/githuband generate a webhook secret (GITHUB_WEBHOOK_SECRET). - Download the App private key and set
GITHUB_APP_IDandGITHUB_APP_PRIVATE_KEY(PEM, base64 accepted). - Install the App on a test repository and open a PR.
GitHub OAuth App
- Create a GitHub OAuth App (Settings → Developer settings → OAuth Apps), separate from the GitHub App above.
- Set the Authorization callback URL to
https://your-host/api/auth/callback. - Set
GITHUB_OAUTH_CLIENT_IDandGITHUB_OAUTH_CLIENT_SECRETfrom the OAuth App page.
Operations
/api/health— database ping, suitable for liveness probes./api/metrics— Prometheus text (queue depth, reviews by status, silence rate, watchdog kills), bearer-protected byMETRICS_TOKEN.- The worker's watchdog fails any review running longer than 10 minutes and completes its check-runs as failed, so a stuck review can never hold a PR hostage as eternally in-progress.
- The CLI binary is baked into the worker image at a pinned commit; upgrading the reviewer is an image upgrade, not a runtime download.