The self-hosted shape
Renovate has no daemon. It's a CLI that scans a list of repos, opens / updates PRs, and exits. Run it periodically — from a GitHub Actions cron job, a self-hosted scheduler, or a dedicated server with a cron entry.
The cron-from-GitHub-Actions pattern (simplest)
# .github/workflows/renovate.yml in a dedicated "renovate-bot" repo
name: Renovate
on:
schedule:
- cron: '0 1,9,17 * * *' # 3x daily
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: renovatebot/github-action@v40
with:
configurationFile: renovate-config.js
token: ${{ secrets.RENOVATE_TOKEN }}
env:
LOG_LEVEL: info
The token is a PAT (classic, with repo + workflow + read:org) or a GitHub App credential.
Configuration
// renovate-config.js (the bot's config, applied to every scanned repo)
module.exports = {
platform: "github",
autodiscover: true, // scan all repos the token has access to
autodiscoverFilter: ["myorg/*"],
onboarding: true, // open an "Onboarding" PR in each repo first time
onboardingConfig: { extends: ["config:recommended"] },
dryRun: null, // "lookup" / "full" for testing
repositoryCache: "enabled",
// Per-language opt-outs / opt-ins via package rules
packageRules: [
{
description: "Group minor + patch npm updates",
matchManagers: ["npm"],
matchUpdateTypes: ["minor", "patch"],
groupName: "npm dependencies",
},
{
description: "Hold major updates for manual review",
matchUpdateTypes: ["major"],
labels: ["major-update"],
automerge: false,
},
],
};
Per-repo customization: renovate.json
Each scanned repo can have its own renovate.json (or .json5, .yml, embedded in package.json) overriding global config. The first run opens an "Onboarding" PR with a default renovate.json proposal; merge it (or edit first) to activate Renovate for that repo.
// renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits"
],
"schedule": ["after 10pm and before 5am every weekday", "every weekend"],
"timezone": "America/Toronto",
"labels": ["dependencies"],
"packageRules": [
{
"description": "Auto-merge non-major dev deps if CI passes",
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
},
{
"description": "Group Tailwind + PostCSS",
"matchPackagePatterns": ["^tailwindcss", "^postcss", "^autoprefixer"],
"groupName": "Tailwind CSS"
}
],
"vulnerabilityAlerts": {
"labels": ["security"],
"automerge": true,
"schedule": "at any time"
}
}
The Dependency Dashboard
With :dependencyDashboard in extends, Renovate opens one long-lived issue per repo summarizing every pending update: open PRs, rate-limited updates, errored manifests, plus checkboxes to manually request immediate refresh of specific deps. The repo's living source of truth for "what's pending."
Schedules + rate limiting
Renovate doesn't dump 50 PRs on you at once:
- Schedule — only open / update PRs during specified windows.
- prConcurrentLimit — max open PRs at any time (default: 10).
- prHourlyLimit — max new PRs per hour (default: 2).
- minimumReleaseAge — only open PRs for releases older than N days (avoids broken-just-released versions).
Automerge with merge-confidence
For dev-only deps with passing CI, automerge is safe enough to leave on. Renovate's merge-confidence data (free public service from Mend) augments this: each version of a package has an adoption-rate + age signal; you can require "high confidence" before auto-merging.
{
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
"matchCurrentVersion": "!/^0/", // skip 0.x packages (less stable)
"matchConfidence": ["high", "very high"],
"automerge": true
}
]
}
Self-hosted on a dedicated server
# systemd-timer-driven (see /tutorials/systemd-timers-cron-replacement.html)
# /etc/systemd/system/renovate.service
[Service]
Type=oneshot
User=renovate
Environment=RENOVATE_TOKEN=<pat>
Environment=RENOVATE_PLATFORM=github
Environment=LOG_LEVEL=info
ExecStart=/usr/local/bin/renovate
# /etc/systemd/system/renovate.timer
[Timer]
OnCalendar=*-*-* 03:00,11:00,19:00:00
Persistent=true
Unit=renovate.service
[Install]
WantedBy=timers.target
Install via npm or Docker:
npm install -g renovate
# or
docker run --rm -e RENOVATE_TOKEN renovate/renovate:latest myorg/repo
For GitLab / Gitea / Bitbucket
The platform is configurable. For Gitea:
{
"platform": "gitea",
"endpoint": "https://gitea.example.com/api/v1/",
"token": "<gitea-token>"
}
Same flow: Renovate scans repos, opens MRs / PRs, auto-merges when configured. Especially useful for self-hosted Gitea where Dependabot doesn't exist.
Patterns worth knowing
Group Kubernetes / Helm / Docker into one PR
{
"packageRules": [{
"matchManagers": ["helm-values", "helmv3", "kubernetes", "docker-compose"],
"groupName": "Kubernetes dependencies",
"schedule": ["before 5am on monday"]
}]
}
Pin GitHub Actions to digests instead of tags
{
"extends": ["helpers:pinGitHubActionDigests"]
}
Every action gets pinned to a specific commit SHA; Renovate updates the SHA + leaves the tag as a comment for human-readability. Protects against re-tagged actions doing surprise things.
Lockfile maintenance
{
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 5am on monday"]
}
}
Periodically re-resolves lockfiles even when no manifest changes — picks up transitive dependency updates that pure manifest-scan wouldn't catch.
Renovate vs Dependabot
- Dependabot — built into GitHub; zero-setup for public-GitHub-only workflows. Limited language coverage (npm, pip, Go modules, Docker, GitHub Actions; basic Terraform). Less configurable.
- Renovate — 60+ ecosystems; massively more configurable; runs anywhere; Dependency Dashboard, merge-confidence, etc. The trade-off is one more thing to operate.
For "this is one or two public-GitHub repos in JS," Dependabot is the right tool. For everything else — polyglot, monorepos, self-hosted Git, Kubernetes manifest updates, Helm chart updates, image tag updates — Renovate.