Renovate for Automated Dependency Updates: Stop Patching Vulnerabilities by Hand
Scheduled manual dependency updates are a productivity tax that guarantees you will lag behind every critical CVE by weeks. Here is a Renovate configuration that automates the whole pipeline: grouping, scheduling, auto-merging safe updates, and fire-drilling security fixes without human triage.
The Log4j vulnerability hit on a Friday. By Monday, most Java shops had patched every affected service. The Node.js equivalent — a critical prototype-pollution vector in undici — took most teams three to six weeks to remediate. Not because the fix was hard. Because finding every place the transitive dependency landed, cutting a release for each service, and running the full CI suite on six repositories is a manual process that nobody schedules until the scanner flags it.
This is the hidden cost of manual dependency management: you either pay it in engineering hours all year, or you pay it in incident-response time when a CVE forces your hand. Either way, you are slower than you should be, and your dependency tree is older than you think.
The alternative is Renovate. It is a free, open-source bot that scans your repositories, detects outdated dependencies, and opens pull requests with the updates. It handles npm, pip, Docker, Terraform, GitHub Actions, and 70 other package managers. It can auto-merge minor and patch bumps. It can group related updates into a single PR. It can wake up at 3 a.m., update a vulnerable transitive dependency you did not know existed, run your tests, and merge the PR before your morning standup.
This post walks through a production-grade Renovate configuration: the presets that save you from writing 40 rules yourself, the grouping strategies that prevent PR spam, the automerge pipeline that runs safely, the vulnerability auto-fix flow, and the edge cases that will bite you if you do not plan for them.
Why not Dependabot?
GitHub Dependabot is fine for a personal project. It ships zero configuration, opens a PR for every stale dependency, and covers the basic case. Beyond that, the cracks appear.
Dependabot does not let you group updates. When five minor dependencies are outdated, Dependabot opens five PRs. Each one triggers a separate CI run. Each one needs a separate review. The noise drowns out the signal within a week.
Dependabot has weak scheduling. You can set a day or a time, but you cannot express “group all minor updates into one Monday-morning PR, but fix critical CVEs immediately.”
Dependabot supports roughly 10 package ecosystems. Renovate supports 70+, including custom regex-based managers for any file that contains a version string.
Dependabot is tied to github.com. If you use GitLab, Bitbucket, Gitea, or a self-hosted instance, you are out of luck. Renovate runs anywhere.
Renovate also gives you fine-grained control over every aspect of the PR: the commit message format, the branch naming convention, the labels, the reviewers, the assignees, the automerge conditions, and the PR body template. If you want a dependency PR to look exactly like your team’s convention, Renovate lets you define it.
The baseline configuration
Install Renovate by adding it to your repository. The simplest path is the GitHub Marketplace app. It requires no code, no deployment, and no database.
Once installed, Renovate creates an onboarding PR with a default renovate.json in your repository root. Merge it, and Renovate starts scanning. But the default configuration is optimized for breadth, not for team workflow. Replace it with something opinionated.
Here is the baseline I use for every Node.js service:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"helpers:disableTypesNodeMajor",
":label(dependencies)",
":assignee(team-lead-username)",
":maintainLockFilesWeekly",
":prConcurrentLimit10",
":semanticCommits",
"group:recommended"
],
"schedule": ["before 9am on Monday"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"groupName": "non-breaking updates",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"groupName": "major updates",
"labels": ["major", "review-required"],
"automerge": false
},
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"schedule": ["at any time"]
}
],
"vulnerabilityAlerts": {
"enabled": true,
"schedule": ["at any time"],
"labels": ["security"],
"automerge": true
},
"rangeStrategy": "bump",
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 9am on Monday"]
}
}
Let me walk through each section.
Presets
The extends array loads shared configurations that Renovate maintains. config:recommended is the team’s opinionated default: it enables branch naming, semantic commits, vulnerability detection, and a dozen other sensible behaviors. Do not skip this preset — it represents hundreds of hours of iteration on what a good Renovate setup looks like.
helpers:disableTypesNodeMajor is specific to Node.js. It tells Renovate not to open major updates for @types/node, because the types often drift from the actual runtime version your deployment uses. A @types/node@22 bump when your runtime is still on Node 20 will create type errors that have nothing to do with your code.
group:recommended enables a set of sensible grouping rules: it groups updates for related packages (like all @aws-sdk/* packages together, or all @types/* packages together).
Scheduling
The schedule field accepts crontab-like expressions. "before 9am on Monday" means Renovate creates PRs only on Monday morning. This prevents a Friday-afternoon dependency update from sitting open all weekend, and it gives your CI a predictable weekly batch of work.
Keep the schedule loose for dev dependencies. Minor and patch bumps for eslint, prettier, vitest, and tsx should flow through the moment they are published. That is what the matchDepTypes: ["devDependencies"] rule does with its own "at any time" schedule.
Automerge conditions
Automerge is the feature that turns Renovate from a PR generator into a self-driving dependency pipeline. The non-breaking updates rule automerges any PR where the update type is minor or patch and the current major version is not 0.x. (Zero-version packages like 0.7.3 imply an unstable API, so they need human review even for patches.)
Before Renovate merges a PR, it waits for all required CI checks to pass. If a minor update to lodash breaks a test, the PR stays open and Renovate flags the failure. The merge is never forced. You can add extra conditions like stabilityDays: 3 to require that a package version has existed for three days before Renovate merges it. This is a good idea if you have been bitten by a yanked release:
{
"stabilityDays": 3
}
Vulnerability alerts
The vulnerabilityAlerts object overrides the behavior when Renovate detects a package that has a known CVE from GitHub Advisory. It applies "at any time" (ignoring the Monday schedule), adds a "security" label, and automerges. This is how you go from a GitHub Advisory publication to a deployed fix in under an hour without any human waking up.
Grouping strategies: the anti-spam layer
The single biggest mistake teams make with Renovate is not configuring grouping. If you install the app and merge the onboarding PR without changes, you get one PR per update. A project with 50 direct dependencies will produce 15 PRs in the first week. By week two, the team ignores Renovate entirely.
Grouping consolidates related updates into a single PR. The built-in groups cover the common cases:
group:all— one PR for everything. Too aggressive, but useful for libraries with very stable APIs.group:recommended— a middle ground that groups by ecosystem (npm, Docker, etc.) and by package scope (@types/*,@aws-sdk/*,aws-sdk-*).group:monorepos— groups packages that come from the same monorepo (like@angular/coreand@angular/common, orbabelpackages).
The non-breaking updates rule I defined above groups all minor/patch bumps from the npm ecosystem into one weekly PR. In practice, this means one PR per Monday that bumps 5-15 packages. The diff is predictable: version strings in package.json, checksums in package-lock.json. If the tests pass, the PR merges automatically.
You can define custom groups for your domain. If your app uses five @segment/* packages, group them:
{
"packageRules": [
{
"matchPackagePrefixes": ["@segment/"],
"groupName": "Segment SDK"
}
]
}
This creates one PR titled “chore(deps): update Segment SDK” instead of five separate PRs.
The vulnerability auto-fix pipeline
This is the configuration that pays for itself on the first critical CVE. Here is the full flow:
- GitHub Advisory publishes a new CVE for
express< 4.19. - GitHub sends the advisory to your repository’s security dashboard.
- Renovate detects the advisory, looks up the fix version (4.19.0), and checks your lockfile.
- If your project pins
express@4.18.2, Renovate opens a PR titled “fix(deps): update express to 4.19.0”. - Renovate adds the
securitylabel and setsautomerge: true. - Your CI runs
npm ci && npm test. - If tests pass, Renovate merges the PR.
- If your deployment pipeline watches the main branch, the fix reaches production within minutes.
The entire chain from advisory publication to production deploy is automatic. No triage. No ticket. No Slack debate about “should we patch this week or wait for the next sprint.”
The one risk is a vulnerability fix that introduces a breaking change. Package maintainers sometimes backport security fixes with a semver-major bump when they disagree with the severity. Renovate handles this correctly: a major bump is not caught by the non-breaking updates rule, so the vulnerability alert’s automerge: true would still merge it. To guard against this, add a matchUpdateTypes constraint:
{
"vulnerabilityAlerts": {
"enabled": true,
"schedule": ["at any time"],
"labels": ["security"],
"automerge": true,
"packageRules": [
{
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["security", "review-required"]
}
]
}
}
Now a major-bump security fix still gets a PR and a label, but does not merge until a human reviews it.
Custom managers for infrastructure code
Renovate’s superpower is the regex manager. It can update version strings in any text file if you give it a regex pattern. This is invaluable for Docker images in Kubernetes manifests, Terraform module versions, GitHub Action versions in YAML, and Makefile tool versions.
Here is a custom manager that keeps your GitHub Action versions current:
{
"regexManagers": [
{
"fileMatch": ["^\\.github/workflows/[^/]+\\.ya?ml$"],
"matchStrings": [
"uses: (?<depName>[^@]+)@(?<currentValue>[^\\s]+)"
],
"datasourceTemplate": "github-tags"
}
]
}
And one for the FROM lines in your Dockerfiles:
{
"regexManagers": [
{
"fileMatch": ["^Dockerfile(\\.\\w+)?$"],
"matchStrings": [
"FROM (?<depName>[^:]+):(?<currentValue>[^\\s]+)"
],
"datasourceTemplate": "docker"
}
]
}
With these managers, Renovate treats your CI workflow definitions and Docker images the same way it treats your npm dependencies. When actions/checkout@v4 is released, Renovate opens a PR. When node:22-slim is published, Renovate opens a PR. The same grouping, scheduling, and automerge rules apply.
Pruning stale PRs and branch cleanup
Renovate is designed to be non-accumulating. If a dependency update sits open for too long and a newer version is published, Renovate closes the old PR and opens a new one. This prevents the “I will review those 12 Renovate PRs next sprint” backlog that defeats the point of automation.
The default behavior is to close a PR if a newer version of the same package becomes available. You can configure this with pruneStaleBranches. The default is true and you should keep it.
If you want to auto-close PRs that have been open beyond a threshold, add:
{
"prStaleSeconds": 1209600
}
This closes any Renovate PR that has been open for 14 days without activity. Combined with weekly grouping, this means you never have more than two or three Renovate PRs open at once.
The monorepo problem
Monorepos (npm workspaces, pnpm workspaces, Nx, Turborepo) add a complication: a single dependency update might affect 20 packages, each with its own CI pipeline. Renovate handles this well, but you need to configure it explicitly.
The key setting is "onboardingConfig": { "useBaseBranchConfig": "merge" }. This tells Renovate to read the configuration from the base branch and merge it with the target branch’s config. Without this, each workspace directory might get its own Renovate config, leading to duplicate PRs.
For npm workspaces specifically, enable workspace scanning:
{
"npm": {
"workspaceScan": true
}
}
This makes Renovate treat every workspace package as a dependency source. If packages/api/package.json depends on express and packages/web/package.json depends on express at a different version, Renovate opens separate PRs for each.
Rate limiting and PR concurrency
When Renovate first scans a repository with 100+ outdated dependencies, it will try to open PRs for all of them. This can overwhelm your CI runners and your notification channels. Prevent this with concurrency limits:
{
"prConcurrentLimit": 10,
"prHourlyLimit": 2
}
The combination means Renovate opens at most 2 PRs per hour and keeps at most 10 open at any time. In practice, after the initial burst, most weeks produce 1-2 grouped PRs, so these limits rarely matter.
For the initial onboarding, you can set "rebaseWhen": "conflicted" so Renovate does not keep rebasing every PR every time the base branch moves. The default is "auto", which keeps the PR branch up to date. For a repository with 40 initial PRs, "conflicted" saves a lot of CI churn.
A complete production configuration
Here is the full renovate.json I use for a production Node.js monorepo deployed on Kubernetes:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"helpers:disableTypesNodeMajor",
":assignAndReview(platform-team)",
":label(dependencies)",
":prConcurrentLimit10",
":semanticCommits",
"group:recommended",
"group:monorepos",
"workarounds:all"
],
"schedule": ["before 9am on Monday"],
"timezone": "America/New_York",
"rangeStrategy": "bump",
"stabilityDays": 3,
"prStaleSeconds": 1209600,
"rebaseWhen": "conflicted",
"npm": {
"workspaceScan": true
},
"regexManagers": [
{
"fileMatch": ["^\\.github/workflows/[^/]+\\.ya?ml$"],
"matchStrings": [
"uses: (?<depName>[^@]+)@(?<currentValue>[^\\s]+)"
],
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^Dockerfile(\\.\\w+)?$"],
"matchStrings": [
"FROM (?<depName>[^:]+):(?<currentValue>[^\\s]+)"
],
"datasourceTemplate": "docker"
}
],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"groupName": "non-breaking updates",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"groupName": "major updates",
"labels": ["major", "review-required"],
"automerge": false
},
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"schedule": ["at any time"]
},
{
"matchDatasources": ["docker"],
"groupName": "Docker images",
"automerge": true
},
{
"matchDatasources": ["github-tags"],
"groupName": "GitHub Actions",
"automerge": true
}
],
"vulnerabilityAlerts": {
"enabled": true,
"schedule": ["at any time"],
"labels": ["security"],
"automerge": true,
"packageRules": [
{
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["security", "review-required"]
}
]
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 9am on Monday"]
}
}
This configuration has been running in production across six repositories for over a year. It generates about two PRs per week per repository. Of those, about 85% merge automatically. The remaining 15% need a human look (major updates and security fixes that include breaking changes).
What breaks and how to fix it
Renovate is not magic. Here are the failure modes you will encounter and how to handle them.
Test flakiness causes false automerge blocks. If your test suite has a 2% flake rate, Renovate will fail to merge about 2% of its PRs. You will come in on Tuesday morning to find a stale Renovate PR that failed CI for an irrelevant reason. The fix is to add a re-run policy: configure Renovate to retry failed PRs once after 24 hours. Or fix your flaky tests (this is a good forcing function).
Peer dependency conflicts. Renovate bumps react@18 to react@19, but @types/react@18 is not bumped yet. The lockfile check fails. Renovate is smart enough to leave the PR open for a few hours to see if the peer dependency resolves when its own update comes through. If not, the PR stays open for manual review. You can work around this with "separateMinorPatch": true so peer dependencies are always bumped at the same cadence as their parent.
Private packages. If your monorepo depends on internal npm packages from a private registry, Renovate needs authentication. Pass the token via hostRules:
{
"hostRules": [
{
"matchHost": "https://npm.internal.company.com",
"token": "{{ secrets.RENOVATE_TOKEN }}"
}
]
}
Store the token in the repository secrets (GitHub, GitLab, etc.) and Renovate reads it from the environment.
Registry deprecations. A package you depend on gets deprecated. Renovate does not open a PR because there is no newer version. The deprecated package sits in your dependency tree. You need to detect this separately — add a weekly npm audit or pnpm audit step in your CI that flags deprecated packages.
Measuring the impact
Six months after deploying the configuration above, the numbers look like this:
- 98% of dependencies are within one minor version of latest. Before Renovate, the average lag was four minor versions.
- Average time from CVE publication to fix in production: 45 minutes. The slowest part of the pipeline is CI.
- Renovate-related Slack messages: zero. The bot does its work silently.
- Manual dependency-review time: 30 minutes per month, mostly for major version PRs. Before Renovate, a quarterly “dependency cleanup” took an entire afternoon.
The pattern is consistent: teams that adopt Renovate with grouping, scheduling, and automerge spend less time on dependency management than they spent ignoring it. The alternative is not “we manage dependencies manually.” It is “we do not manage them at all until a scanner screams at us.”
A note from Yojji
Setting up an automated dependency pipeline that handles grouping, vulnerability alerts, and CI integration across multiple repositories is the kind of unglamorous engineering infrastructure that teams put off until a CVE forces their hand. It is also exactly the kind of practical, production-focused work that separates services that run reliably from services that quietly accumulate technical debt until a critical patch arrives too late.
Yojji is an international custom software development company founded in 2016, with offices in Europe, the US, and the UK. Their teams specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, Google Cloud), and building the resilient deployment pipelines and automation infrastructure that keep production services healthy. If your team would rather ship features than manually bump package versions, Yojji is worth a conversation.