Postil

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:migrate

The 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_ID and GITHUB_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-coder

Azure 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:30b

Then 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:30b

If 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

  1. Create a GitHub App on your org with permissions contents: read, pull_requests: write, checks: write, metadata: read, and the pull_request, installation, and installation_repositories events. For the interactive @postil bot, also add issues: write, issue_comment, and pull request review comment events.
  2. Set the webhook URL to https://your-host/api/webhooks/github and generate a webhook secret (GITHUB_WEBHOOK_SECRET).
  3. Download the App private key and set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY (PEM, base64 accepted).
  4. Install the App on a test repository and open a PR.

GitHub OAuth App

  1. Create a GitHub OAuth App (Settings → Developer settings → OAuth Apps), separate from the GitHub App above.
  2. Set the Authorization callback URL to https://your-host/api/auth/callback.
  3. Set GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET from 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 by METRICS_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.