Installation
The ReviewGate bot ships as a single Docker image. Redis comes up next to it (the review queue); PostgreSQL is optional and only stores review metrics. All of it runs inside your own network. The second surface — the CLI for a developer machine — ships as a separate executable with no Docker and no Node: CLI, hook and MCP.
app — the bot itself · redis — the BullMQ queue (required: the webhook only enqueues a job, the worker does all the work) · postgres — optional, review metrics only (off by default; the bot works fully without it). Your code and diffs are never stored.Running it
Download the ready-made client docker-compose.yml (the bot plus Redis, optionally Postgres), fill in .env next to it (see the reference below) and bring it up:
curl -O https://reviewgate.dev/docker-compose.yml
docker compose up -d
docker compose ps # every service healthy
docker compose logs -f appCheck that the bot answers:
curl http://localhost:3000/api/health
# {"status":"ok","ts":...,"version":"0.1.x",
# "capacity":{"total":3,"cli":1,"mrReserve":2,
# "inFlight":{"mr":0,"cli":0},"waiting":0}}capacity is how the review throughput is set up, readable from outside without going onto the machine for the startup log: total is the number of reviews the worker runs in parallel (REVIEW_CONCURRENCY), cli is how many of those slots local runs through the review server may take, mrReserve is the slots they can never take — the guarantee that a request review starts at once — and inFlight/waiting are what is happening right now. Note what the answer does not tell you: it is produced without touching Redis or the database, so a green reply means the process is up, not that the queue is reachable.
docker compose logs app.Environment: GitLab
| variable | what it does | example / default |
|---|---|---|
| GITLAB_BASE_URL | the URL of your GitLab instance, without /api/v4 and without a trailing slash | https://gitlab.acme.com |
| GITLAB_TOKEN | the bot token with the api scope | glpat-… |
| GITLAB_WEBHOOK_SECRET | the webhook secret; the same value goes into the project webhook (8 characters or more) | — |
| GITLAB_TIMEOUT_MS | timeout of a request to the GitLab API | 15000 |
A vendor counts as configured once GITLAB_TOKEN is set. The bot works with one vendor or with both at once — but at least one must be configured, otherwise it will not start.
Environment: GitHub
The second hosting vendor: reviewing pull requests on github.com. Configuring it is not required — the whole block is optional. Connecting the vendor is described separately in Connecting GitHub.
| variable | what it does | example / default |
|---|---|---|
| GITHUB_WEBHOOK_SECRET | the HMAC signing key for webhooks (X-Hub-Signature-256), 8 characters or more | — |
| GITHUB_APP_ID | the GitHub App ID — the recommended mode | — |
| GITHUB_APP_PRIVATE_KEY_B64 | the App private key, base64 on a single line (base64 -w0 key.pem) | — |
| GITHUB_TOKEN | an alternative to the App: a personal access token (quick start) | — |
| GITHUB_BASE_URL | the address of the installation | https://github.com |
| GITHUB_TIMEOUT_MS | timeout of a request to the GitHub API | 15000 |
GITHUB_WEBHOOK_SECRETand authentication — either the pair GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY_B64 or GITHUB_TOKEN. A lone GITHUB_TOKEN deliberately does not switch the vendor on: it is the most ambient variable in the ecosystem (the gh CLI and Actions both export it), and otherwise a GitLab-only deployment would fail at startup because of somebody else's token.Environment: Redis and the queue
| variable | what it does | example / default |
|---|---|---|
| REDIS_HOST | the Redis host (the service name in docker-compose) | redis |
| REDIS_PORT | the Redis port | 6379 |
| REDIS_PASSWORD | the Redis password, if one is set | — |
| REVIEW_CONCURRENCY | how many reviews the worker runs in parallel (1–64). Raise it for a large team's peaks — the queue drains proportionally faster; for a local model, lower it instead. Within that range the ceiling is set not by the worker but by the throughput of your model: see the note on running one locally | 3 |
Environment: the model
The details, and running a model locally, are in Models and your own key.
| variable | what it does | example / default |
|---|---|---|
| LLM_PROVIDER | the provider: anthropic · yandex · ollama · openai · openrouter · vllm | anthropic |
| LLM_API_KEY | the provider key (generic name; ANTHROPIC_API_KEY is an alias). Without it the bot starts, but a review fails with a clear error | — |
| LLM_MODEL | the model (generic name; ANTHROPIC_MODEL is an alias); a cheaper one works too | claude-opus-4-8 |
| LLM_BASE_URL | the endpoint of an OpenAI-compatible provider or gateway (ANTHROPIC_BASE_URL is an alias; empty = the vendor cloud) | — |
| LLM_FOLDER_ID | the Yandex Cloud folder — required when LLM_PROVIDER=yandex | — |
| LLM_DATA_LOGGING | let Yandex AI Studio log the requests on its side | false |
| LLM_PROXY | a CONNECT proxy to reach the provider (a blind tunnel; empty = direct) | — |
| LLM_TIMEOUT_MS | timeout of a request to the model, ms (local models answer slowly) | 600000 |
| LLM_JSON_MODE | how to ask an OpenAI-compatible provider for a structured answer: schema (json_schema) · object (json_object) · none (the prompt and the parser hold the shape). DeepSeek: none — it rejects json_schema, and json_object returns an empty answer on a real review run (a long prompt). Per backend: LLM_BACKEND_<NAME>_JSON_MODE | schema |
| LLM_EFFORT | reasoning depth: low · medium · high · xhigh · max | high |
| LLM_MAX_TOKENS | the cap on output tokens | 16000 |
| LLM_CACHE_TTL | TTL of the system prompt cache (Anthropic only): 1h · 5m · off; 1h saves input tokens on a stream of pull requests | 1h |
| LLM_DIFF_CHAR_BUDGET | the character budget for the diff in the prompt (~100K tokens) | 400000 |
| LLM_FULL_FILE_CONTEXT | recall: whole changed files plus their neighbours for the generator, not only the diff (opt-in) | false |
| LLM_ENVIRONMENT_CONTEXT | recall: stack versions from the manifests, added to the prompt (opt-in) | false |
| LLM_GENERATORS | the deployment default generator roles, comma-separated: [backend:]model[@effort] (the first is the main one; default means the main provider). Per repository, llm.generators replaces it entirely | — |
| LLM_JUDGES | the default judge panel (same syntax, at most 2); per repository, llm.judges | — |
| LLM_ARBITER | the default arbiter, chairing the panel (a single role); per repository, llm.arbiter | — |
| LLM_BUDGET_TOKENS | the spending cap: the token budget of a RUN for the main provider (input + output + cache). The thresholds are fixed and not configurable: 80% — a notice in the summary, 100% — a loud notice (the review still completes), 300% — the run stops and publishes what it has. Other vendors: LLM_BACKEND_<NAME>_BUDGET_TOKENS; without the variable a backend is unlimited | — |
| DIAGNOSTICS | the 🔬 run diagnostics block in the summary — the deployment default; per repository, diagnostics in config.yml overrides it | false |
| REPLY_ENABLED | reply mode: the bot answers replies in the threads of its own review (the webhook needs the Comments trigger) — the deployment default; per repository, reply.enabled in config.yml overrides it | false |
Environment: Postgres (optional)
Postgres is only needed for review metrics — it is off by default and the bot works fully without it. To enable it, uncomment the postgres service and the DATABASE_URL line in docker-compose.yml, then set a password in .env. The metrics hold counters and metadata only (how many findings were generated, kept and dropped by the judge; the models, the tokens) in your own database, cleaned up after 90 days — a convenient way to see what the bot is worth and what the judge drops.
| variable | what it does | example / default |
|---|---|---|
| DATABASE_URL | the Postgres connection string; empty = metrics off, the bot still works | postgres://reviewgate:password@postgres:5432/reviewgate |
| POSTGRES_PASSWORD | the database password (in .env); used both in DATABASE_URL and by the postgres service | reviewgate |
Environment: license
| variable | what it does | example / default |
|---|---|---|
| REVIEWGATE_LICENSE | a previously issued JWT key (optional); without it the bot runs with the «License: Community» status in the summary (the language follows the language key of the config) | — |
| REVIEWGATE_LICENSE_PUBLIC_KEY | overrides the built-in public key (rarely needed) | — |
Environment: logs (optional)
The recipe for shipping to Graylog, ELK or Loki is on Logs and monitoring.
| variable | what it does | example / default |
|---|---|---|
| LOG_FORMAT | log format: text · json (one JSON line per record, with the job attributed by project_path/mr_iid/head_sha/vendor for your log stack; strictly lower case) | text |
Where the image comes from
docker compose up -d pulls the image on its own — the commands below are only needed if you fetch it by hand: into an air-gapped network, into your internal registry, or to inspect it before installing.
| address | role |
|---|---|
registry.reviewgate.dev/reviewgate-bot | the primary one; no docker login needed — reads are open |
novohudonossor/reviewgate-bot | a Docker Hub mirror, the same image (the digest matches) |
Ask the registry itself which versions are published:
curl -s https://registry.reviewgate.dev/v2/reviewgate-bot/tags/listPull it by hand and verify you got the right image — the digest must match the mirror, since the image is built once and published to both registries:
docker pull registry.reviewgate.dev/reviewgate-bot:latest
# the digest must match the mirror:
docker buildx imagetools inspect registry.reviewgate.dev/reviewgate-bot:latest | grep Digest
docker buildx imagetools inspect novohudonossor/reviewgate-bot:latest | grep DigestAir-gapped network (no internet)
On a machine with internet access save the image to a file, move it by whatever means your policy allows, and load it on the target server. The image is built for linux/amd64 — pass the platform explicitly if you pull from an ARM machine, a Mac for example:
VERSION=latest # or a specific tag from the list above
# 1) on a machine with internet access
docker pull --platform linux/amd64 registry.reviewgate.dev/reviewgate-bot:$VERSION
docker save registry.reviewgate.dev/reviewgate-bot:$VERSION | gzip > reviewgate-bot-$VERSION.tar.gz
# 2) move the file to the target server, then there:
gunzip -c reviewgate-bot-$VERSION.tar.gz | docker load
docker image ls | grep reviewgate-bot After docker load, put the tag you loaded into docker-compose.yml — then up -d takes the local image and never reaches for the network.
Upgrading
The bot is distributed as a public image from registry.reviewgate.dev (mirrored on Docker Hub, see The image will not pull) — we recommend pinning a specific version tag. Upgrading is an ordinary pull and restart:
docker compose pull app
docker compose up -d appIf the tag is pinned (not latest), put the new one into docker-compose.yml first — otherwise pull fetches the same image and up -d recreates nothing. The list of published versions comes from the command in Where the image comes from.
Which version is running
The build version is baked into the image and shows up in the /api/health response, in the 🚀 ReviewGate bot … line of the startup log, and in the output of diagnose.sh.
curl -s http://localhost:3000/api/health
# {"status":"ok","ts":...,"version":"0.1.x","capacity":{...}}
# ↑ the version of the running image
docker compose logs app | grep 🚀
# 🚀 ReviewGate bot 0.1.x listening on http://0.0.0.0:3000/apiIf the bot does not start at all — because of an error in .env, say — the version is still readable: it lives in the image, not in the running application:
docker compose run --rm --no-deps app printenv REVIEWGATE_VERSION
# 0.1.xdocker-compose.yml shows what you asked for, not what is actually running: with latest it never changes from one upgrade to the next, and when you mirror the image into your own registry you choose the tag yourself. The version field is stamped at build time, so it survives both. A value of dev means a build from source rather than from a published image.Include the version in any support request — that is where the investigation starts.