Mergepatrol

Self-host Mergepatrol

Run it

GitHub webhooks wake a queue as soon as something changes, but GitHub only delivers them to a public https URL. On localhost, Mergepatrol polls.

  1. Generate the secret key. It encrypts the GitHub App and the model API keys in the database. Keep it: without it, you must set Mergepatrol up again.

    export MERGEPATROL_SECRET_KEY=$(openssl rand -base64 32)
    
  2. Start Mergepatrol:

    docker compose up --build -d
    docker compose logs mergepatrol | grep setup_token
    

    Or without Docker for Mergepatrol itself, with Rust (rust-toolchain.toml picks the version) and Node 24 with pnpm:

    docker compose -f compose.dev.yml up -d   # or any Postgres 17
    (cd ui && pnpm install && pnpm build)     # the dashboard, which the binary embeds
    export MERGEPATROL_DATABASE_URL=postgres://mergepatrol:mergepatrol@127.0.0.1:54329/mergepatrol
    cargo run -p mergepatrol                  # prints the setup token
    

    Jobs then run as local processes, without a sandbox. If port 8080 is taken, set MERGEPATROL_BIND=127.0.0.1:<port> and MERGEPATROL_PUBLIC_URL=http://localhost:<port>.

  3. Open http://localhost:8080 and do the setup in the dashboard:

    1. Enter the setup token from the logs.
    2. Create the GitHub App. GitHub creates it with the permissions Mergepatrol needs. If the button does nothing, the name is taken, even by an App you deleted: change it.
    3. Install the App on your organization or account, and choose its repos. GitHub sends you back to sign in.
    4. In Repos, set up the repos to queue. Each gets a PR that adds its config file: see Repo settings. Merge it, and the repo queues.
    5. In Settings, add your Anthropic or OpenAI API key. Mergepatrol checks it with the provider, then stores it encrypted. Create a key for Mergepatrol alone, with a spend limit (for Anthropic, in a workspace with one), so a leaked key costs at most the limit. For OpenAI, you can Sign in with ChatGPT instead, and Codex runs on your ChatGPT plan. Mergepatrol signs in the way the Codex CLI does, which OpenAI does not support for other apps and could stop at any time: keep an API key to switch back to.
  4. Add the mergepatrol label to a PR to queue it. Mergepatrol creates the queue labels on each repo it queues.

Anyone with a GitHub account can sign in. Each installation of the App is a workspace, with its own repos and its own model API keys. A user sees the workspaces that GitHub lets them access. A session reads them from GitHub again every hour, so a new installation shows up, and a lost one goes, within the hour. A session lasts 30 days from its last use, and ends when GitHub stops taking the user’s token: when they revoke it, or after 6 months, when GitHub’s refresh token expires.

Environment variables

If you reach Mergepatrol at another address than http://localhost:8080, set MERGEPATROL_PUBLIC_URL before you create the App: GitHub sends users back to it, and delivers webhooks there when it is https. To get webhooks on your machine, run a tunnel such as cloudflared tunnel --url http://localhost:8080 and use its URL.

VariableDefaultWhat it does
MERGEPATROL_DATABASE_URL(required)Postgres. Compose runs one
MERGEPATROL_SECRET_KEY(required)Encrypts the stored secrets. 32 bytes, base64. To rotate it, see Rotate the secret key
MERGEPATROL_SECRET_KEY_FILEA file that holds the secret key, instead of MERGEPATROL_SECRET_KEY. Compose uses one
MERGEPATROL_PUBLIC_URLhttp://localhost:8080Where users reach the dashboard
MERGEPATROL_LABELmergepatrolThe default trigger label. <label>:priority jumps the queue, and ejected PRs get <label>:failed
MERGEPATROL_POLL_SECS60How often to poll GitHub, in case a webhook is lost
MERGEPATROL_BATCH_SIZE8The default batch size: the most PRs tested together. 1 merges one PR at a time
MERGEPATROL_MAX_JOBS4The most rebase and fix jobs that run at once, across every queue
MERGEPATROL_DATA_DIR./dataThe repo caches and the job outputs, with their transcripts. A job’s output is kept 7 days
MERGEPATROL_BIND127.0.0.1:8080The dashboard address
MERGEPATROL_PROVIDERanthropicThe resolver’s default provider: anthropic or openai
MERGEPATROL_MODELclaude-opus-5-5The resolver’s default model. Required when the default provider is not anthropic
MERGEPATROL_EFFORTmediumThe resolver’s default effort

Run it with an App you made yourself

The hosted Mergepatrol uses one public GitHub App, which we create by hand on GitHub, rather than with the setup in the dashboard. A self-hosted Mergepatrol can do the same. Set the App’s credentials, and the setup token and the App creation are skipped:

VariableWhat it is
MERGEPATROL_GITHUB_APP_IDThe App ID
MERGEPATROL_GITHUB_APP_PRIVATE_KEYThe private key, PEM
MERGEPATROL_GITHUB_CLIENT_IDThe OAuth client ID
MERGEPATROL_GITHUB_CLIENT_SECRETThe OAuth client secret
MERGEPATROL_GITHUB_WEBHOOK_SECRETThe webhook secret. Optional: without it, Mergepatrol only polls

The App needs the permissions in crates/github/src/setup.rs (manifest), the callback URL <public URL>/auth/callback, the setup URL <public URL>/setup/installed, and the webhook URL <public URL>/webhooks/github, subscribed to the events in WEBHOOK_EVENTS in the same file.

Rotate the secret key

Generate a new key, and set it in front of the old one, separated by a comma: MERGEPATROL_SECRET_KEY=<new>,<old>. Restart Mergepatrol. At startup, it re-encrypts every stored secret with the new key, and logs how many. Then set only the new key, and restart again. Each session’s GitHub token is re-encrypted at its next hourly check; a session idle through the whole rotation signs out.

On GCP, a deploy runs the old revision beside the new one for a while, and each must read what the other writes. Add each step as a version of the mergepatrol-secret-key secret, and deploy it:

  1. <old>,<new>: every revision can read both keys, and still encrypts with the old one.
  2. <new>,<old>: the new revision re-encrypts with the new key.
  3. <new>,<old> again, once the revision from step 1 has stopped: it re-encrypts what that one wrote with the old key meanwhile.
  4. <new>.

How jobs are isolated

A rebase job runs the repo’s code (the verify commands) and an agent with a shell. Mergepatrol treats it as untrusted:

In production the job containers use gVisor (runsc) on a network that cannot reach the cloud metadata server, private ranges, or the host. deploy/sandbox-host.sh sets that up on a Linux VM. Then set:

VariableDefaultWhat it does
MERGEPATROL_EXECUTORlocaldocker runs each job in a container. agent-sandbox runs it in a GKE sandbox: see On GCP. local runs it as a process, without isolation, for development
MERGEPATROL_JOB_IMAGEmergepatrol-job:latestThe job image (docker build --target job)
MERGEPATROL_JOB_RUNTIMEDocker’s defaultrunsc for gVisor
MERGEPATROL_JOB_NETWORKDocker’s defaultmergepatrol-jobs, from deploy/sandbox-host.sh
MERGEPATROL_JOB_RESOLV_CONFDocker’s defaultA file on the Docker host, mounted as the jobs’ /etc/resolv.conf. /etc/mergepatrol/job-resolv.conf on the sandbox network
MERGEPATROL_JOB_PROXY_URLMERGEPATROL_PUBLIC_URLWhere jobs reach the orchestrator: its model API proxy, and a GKE sandbox’s link. http://10.213.0.1:8081 on the sandbox network
MERGEPATROL_JOB_PROXY_BINDNoneA second listener that serves only what jobs use, for jobs that cannot reach the dashboard’s. 0.0.0.0:8081 in compose.yml
MERGEPATROL_JOB_MEMORY4gMemory limit per job
MERGEPATROL_JOB_CPUS2CPU limit per job

The job image has git, the Claude and Codex CLIs, Rust, Go, Python, and Node with pnpm, for the verify commands.

Each release publishes both images, tagged with its version and latest: ghcr.io/agent-labs-dev/mergepatrol, the orchestrator, and ghcr.io/agent-labs-dev/mergepatrol-job. To run a release instead of building, set MERGEPATROL_JOB_IMAGE=ghcr.io/agent-labs-dev/mergepatrol-job:<version>.

Docs