installation

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.

what runsapp — the bot itself · redis — the BullMQ queue (required: the webhook only enqueues a job, the worker does all the work) · postgresoptional, 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:

terminal
curl -O https://reviewgate.dev/docker-compose.yml
docker compose up -d
docker compose ps        # every service healthy
docker compose logs -f app

Check that the bot answers:

terminal
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.

if it does not come upEvery required variable is validated at startup — the bot fails immediately with a clear message about what is missing. Look at docker compose logs app.

Environment: GitLab

variablewhat it doesexample / default
GITLAB_BASE_URLthe URL of your GitLab instance, without /api/v4 and without a trailing slashhttps://gitlab.acme.com
GITLAB_TOKENthe bot token with the api scopeglpat-…
GITLAB_WEBHOOK_SECRETthe webhook secret; the same value goes into the project webhook (8 characters or more)
GITLAB_TIMEOUT_MStimeout of a request to the GitLab API15000

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.

variablewhat it doesexample / default
GITHUB_WEBHOOK_SECRETthe HMAC signing key for webhooks (X-Hub-Signature-256), 8 characters or more
GITHUB_APP_IDthe GitHub App ID — the recommended mode
GITHUB_APP_PRIVATE_KEY_B64the App private key, base64 on a single line (base64 -w0 key.pem)
GITHUB_TOKENan alternative to the App: a personal access token (quick start)
GITHUB_BASE_URLthe address of the installationhttps://github.com
GITHUB_TIMEOUT_MStimeout of a request to the GitHub API15000
when the vendor switches onYou need 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

variablewhat it doesexample / default
REDIS_HOSTthe Redis host (the service name in docker-compose)redis
REDIS_PORTthe Redis port6379
REDIS_PASSWORDthe Redis password, if one is set
REVIEW_CONCURRENCYhow 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 locally3

Environment: the model

The details, and running a model locally, are in Models and your own key.

variablewhat it doesexample / default
LLM_PROVIDERthe provider: anthropic · yandex · ollama · openai · openrouter · vllmanthropic
LLM_API_KEYthe provider key (generic name; ANTHROPIC_API_KEY is an alias). Without it the bot starts, but a review fails with a clear error
LLM_MODELthe model (generic name; ANTHROPIC_MODEL is an alias); a cheaper one works tooclaude-opus-4-8
LLM_BASE_URLthe endpoint of an OpenAI-compatible provider or gateway (ANTHROPIC_BASE_URL is an alias; empty = the vendor cloud)
LLM_FOLDER_IDthe Yandex Cloud folder — required when LLM_PROVIDER=yandex
LLM_DATA_LOGGINGlet Yandex AI Studio log the requests on its sidefalse
LLM_PROXYa CONNECT proxy to reach the provider (a blind tunnel; empty = direct)
LLM_TIMEOUT_MStimeout of a request to the model, ms (local models answer slowly)600000
LLM_JSON_MODEhow 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_MODEschema
LLM_EFFORTreasoning depth: low · medium · high · xhigh · maxhigh
LLM_MAX_TOKENSthe cap on output tokens16000
LLM_CACHE_TTLTTL of the system prompt cache (Anthropic only): 1h · 5m · off; 1h saves input tokens on a stream of pull requests1h
LLM_DIFF_CHAR_BUDGETthe character budget for the diff in the prompt (~100K tokens)400000
LLM_FULL_FILE_CONTEXTrecall: whole changed files plus their neighbours for the generator, not only the diff (opt-in)false
LLM_ENVIRONMENT_CONTEXTrecall: stack versions from the manifests, added to the prompt (opt-in)false
LLM_GENERATORSthe 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_JUDGESthe default judge panel (same syntax, at most 2); per repository, llm.judges
LLM_ARBITERthe default arbiter, chairing the panel (a single role); per repository, llm.arbiter
LLM_BUDGET_TOKENSthe 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
DIAGNOSTICSthe 🔬 run diagnostics block in the summary — the deployment default; per repository, diagnostics in config.yml overrides itfalse
REPLY_ENABLEDreply 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 itfalse

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.

variablewhat it doesexample / default
DATABASE_URLthe Postgres connection string; empty = metrics off, the bot still workspostgres://reviewgate:password@postgres:5432/reviewgate
POSTGRES_PASSWORDthe database password (in .env); used both in DATABASE_URL and by the postgres servicereviewgate

Environment: license

variablewhat it doesexample / default
REVIEWGATE_LICENSEa 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_KEYoverrides the built-in public key (rarely needed)

Environment: logs (optional)

The recipe for shipping to Graylog, ELK or Loki is on Logs and monitoring.

variablewhat it doesexample / default
LOG_FORMATlog 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.

addressrole
registry.reviewgate.dev/reviewgate-botthe primary one; no docker login needed — reads are open
novohudonossor/reviewgate-bota Docker Hub mirror, the same image (the digest matches)

Ask the registry itself which versions are published:

terminal
curl -s https://registry.reviewgate.dev/v2/reviewgate-bot/tags/list

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

terminal
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 Digest

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

machine with internet → air-gapped network
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:

terminal
docker compose pull app
docker compose up -d app

If 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.

terminal
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/api

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

terminal
docker compose run --rm --no-deps app printenv REVIEWGATE_VERSION
# 0.1.x
why not by the tagThe tag in your docker-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.