Self-hosted
"Self-hosted" means two different things depending on what you need. Most teams only need the first one.
Most self-hosting is just the CLI
Postil is one binary. Run it in your own CI with the inference endpoint you choose. Nothing goes to postil.dev; diffs go only to the model endpoint you configure. Use a local Ollama, vLLM, SGLang, or LiteLLM endpoint when diffs must stay inside infrastructure you control:
curl -fsSL https://postil.dev/install.sh | sh
export MODEL_API_KEY=...
export POSTIL_API_KEY="$MODEL_API_KEY"
postil review --repo owner/name --pr 123This is what the quickstart walks through for local runs and GitHub Actions, and what the forges pages cover for GitLab CI, Bitbucket Pipelines, and Azure Pipelines. There is no server to run, nothing to keep patched, and no seat limit: it is a CLI invocation in a job you already have. If that is what you came here for, stop here and go set it up.
Hosting the control plane
The rest of this page is for organizations that also want the bot experience (inline PR comments posted automatically, the @postil mention bot, a dashboard, webhook-driven reviews) running on their own infrastructure instead of postil.dev. That means standing up the same stack we run hosted: Postgres, the web app, and the worker. The stack is Apache-2.0 with no seat fees or license cost; you supply inference and infrastructure. The path is scripted: clone, configure the required secrets, start Compose, and open a test PR. The marketing site at postil.dev is irrelevant to this path: you are replacing it, not depending on it.
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 Docker image bakes in the reviewer CLI from a binary you supply at vendor/postil in the build context; the Dockerfile does not fetch or verify a release itself, so the build fails clearly if that file is missing. Download the release matching POSTIL_CLI_REV in docker-compose.yml (verify its checksum and Sigstore signature, both published alongside the release) and place it at vendor/postil before running docker compose up -d.
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.
Database choice
Postil expects PostgreSQL. The schema uses enums, jsonb,bytea, identity columns, and a queue claimed with FOR UPDATE SKIP LOCKED. SQLite-style hosted databases can work only after a queue and schema rewrite; they are not drop-in replacements for the hosted control plane.
For a free-tier managed Postgres, Supabase Free works with the low-idle queue profile in .env.example. Webhooks kick a bounded web-process queue drain, while the worker stays as a slow fallback. Set WORKER_CONCURRENCY=1, WORKER_IDLE_POLL_MAX_MS=900000, and WORKER_WATCHDOG_INTERVAL_MS=900000 so idle periods stay quiet instead of issuing database checks every few seconds indefinitely. Leave WORKER_HEARTBEAT_INTERVAL_MS unset unless the private monitor is enabled.
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
- The optional
monitoringCompose profile runs a separate monitor process. Start it withdocker compose --profile monitoring up -dafter settingPOSTIL_PUBLIC_URL,POSTIL_OPERATOR_ALERT_EMAIL,BREVO_API_KEY, andWORKER_HEARTBEAT_INTERVAL_MS. The monitor and worker heartbeat produce periodic Postgres traffic. In Brevo, enable anonymous tracking for transactional email and set the shortest operationally useful transactional-log retention in the Brevo account. The monitor stores leases, pass history, process heartbeats, incidents, and delivery attempts in Postgres. Only allowlisted operators can read that state on/operator. POSTIL_SESSION_SECRET: signs session cookies.openssl rand -hex 32.POSTIL_PUBLIC_URL: canonical HTTPS origin for absolute browser URLs and request telemetry, for examplehttps://your-host. Set an origin only, without a path, query, fragment, or credentials.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 BYOK credentials 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.
BREVO_API_KEYenables transactional email. The sender defaults to[email protected]and can be changed withPOSTIL_EMAIL_FROM_EMAILandPOSTIL_EMAIL_FROM_NAME. SetPOSTIL_OPERATOR_ALERT_EMAILto a verified operator inbox for account, installation, billing, and service-monitor alerts. Brevo controls transactional-email tracking and log retention at the account level. Enable anonymous transactional-email tracking in Brevo when you do not need recipient-level open or click events.
Pointing it at a model
These are worker variables. MODEL_API_KEY is preferred; POSTIL_API_KEY and OPENROUTER_API_KEY remain accepted aliases. Set POSTIL_API_KEY to the same value in self-hosted .env files so direct pinned CLI commands such as postil doctor keep working. 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_FORMAT=openai-compatible
MODEL_API_KEY=sk-or-v1-...
POSTIL_API_KEY=sk-or-v1-...
REVIEW_MODEL=z-ai/glm-5.2
REVIEW_MODEL_CASCADE=moonshotai/kimi-k2.7-code,deepseek/deepseek-v4-flashAnthropic
POSTIL_API_BASE=https://api.anthropic.com/v1
POSTIL_API_FORMAT=anthropic
MODEL_API_KEY=sk-ant-...
POSTIL_API_KEY=sk-ant-...
REVIEW_MODEL=claude-sonnet-4-5A private gateway can require one additional header. Set both POSTIL_ENDPOINT_AUTH_HEADER and POSTIL_ENDPOINT_AUTH_VALUE. The value is treated as a secret and is not passed on the command line.
Azure OpenAI
POSTIL_API_BASE=https://azure-resource.openai.azure.com/openai/v1
MODEL_API_KEY=azure-api-key
POSTIL_API_KEY=azure-api-key
REVIEW_MODEL=my-deploymentOllama (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_ALLOW_PRIVATE_API_BASE=1
MODEL_API_KEY=ollama # any non-empty value
POSTIL_API_KEY=ollama # same value for direct postil doctor
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 with POSTIL_ALLOW_PRIVATE_API_BASE=1 (add extra_hosts: ["host.docker.internal:host-gateway"] to the worker service on Linux).
The worker supports OpenAI-compatible chat completions and Anthropic Messages. OpenAI-compatible servers such as vLLM, LiteLLM, SGLang, and TGI use the same configuration shape. The models guide lists current hosted and local recommendations plus the live benchmark command.
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, MODEL_API_KEY, and POSTIL_API_KEY from the container env, so set those in .env before running it. A captured successful run reports:
docker compose exec worker postil doctor
[ok ] config loaded from defaults (model: local-doctor-probe, gate failOn: error, minConfidence: 0.6)
[ok ] git inside a git work tree
[ok ] api key POSTIL_API_KEY, OPENROUTER_API_KEY, MODEL_API_KEY, LLM_API_KEY is set (value not shown)
[ok ] model endpoint http://127.0.0.1:3117/v1 answered for model local-doctor-probe
[ok ] forge tokens presence only: GITHUB_TOKEN unset, GITLAB_TOKEN unset (only needed for remote review)
postil doctor: ready.This transcript was captured from the CLI against a loopback OpenAI-compatible endpoint. Provider URLs and model names differ in your deployment, but the same checks are reported separately: config resolution, git work-tree state, API-key presence, model-endpoint reachability, model readiness, and forge tokens. Every failure names the failing layer and suggests a fix.
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,members: read,issue_comment, and pull request review comment events. Also add thecheck_runevent so the "Re-run" button on a failedpostil/gateorpostil/reviewcheck re-enqueues the review instead of requiring a new push. - 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). - Set
GITHUB_APP_SLUGto the App page's URL slug. Postil uses it to recognize its own inline review threads. - 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
Monitoring, health checks, and metrics for the control plane once it is running.
/api/health: cheap web-process liveness, suitable for container and proxy health checks./api/health/dependencies: dependency readiness check that returns 503 when Postgres is unavailable./api/metrics: Prometheus text (queue depth, reviews by status, 24-hour activity, jobs, sessions, installations, database-up signal), bearer-protected byMETRICS_TOKEN.- PostHog analytics are optional. Set
POSTHOG_PROJECT_TOKENfor server-side request telemetry and runtime-gated browser analytics. Enable Cookieless server hash mode and IP discard in the PostHog project before settingPOSTHOG_CLIENT_CAPTURE=1. Browser capture stores no cookies or browser-persistent identifiers, honors DNT/GPC, and is limited to public marketing, docs, blog, install, pricing, and comparison pages. The server event sends the public path, referrer origin, bounded campaign labels, user agent, and Cloudflare country and bot classification when present; it does not send IP addresses, request identifiers, arbitrary query strings, or protected dashboard paths. - Operational PostHog telemetry is separate and disabled by default. Set
POSTHOG_ERROR_CAPTURE=1for scrubbed exceptions at the web request, worker boot, and exhausted job boundaries, plus fixed classifications for typed model incidents and exact operational sentinel findings after successful review ingestion. SetPOSTHOG_LOG_CAPTURE=1for allowlisted OTLP log events. Sampling defaults to 1% for informational events and 10% for warnings; errors are unsampled.POSTHOG_LOG_MAX_PER_MINUTEandPOSTHOG_ERROR_MAX_PER_HOURimpose per-process hard caps. These paths exclude request data, identities, repository names, prompts, diffs, code, findings, model output, raw error messages, and arbitrary properties. Postil does not upload source maps because PostHog's supported upload path includes application source content. - Scrape
/api/metricsconservatively on small database tiers, for example every few minutes rather than every few seconds. The endpoint is bearer-protected, but each scrape still performs database reads. - The private monitor checks public availability, worker liveness, review and job age, terminal check cleanup, webhook recovery, trial entitlement and signup alerts, billing reconciliation, operator email delivery, and recent provider/model incidents. It does not create GitHub issues, comments, checks, or workflow artifacts.
- The worker's watchdog fails any review running longer than 10 minutes and completes its check-runs as failed, so a stuck review never leaves a PR stuck in progress indefinitely.
- The CLI binary is baked into the worker image at a pinned commit; upgrading the reviewer is an image upgrade, not a runtime download.
- Schema migrations run with
docker compose exec web bun run db:migrate(Drizzle). Run it once after the initialupand again after every upgrade that changes the schema, before traffic hits the new image.