Outdated dependencies are a slow-burning fire. You don’t notice until something breaks — a security advisory drops, a transitive dep pulls in a breaking change, or you realize your package.json is stuck two years in the past. Most teams ignore this problem until it hurts.
Renovate solves it elegantly: it scans your repos, opens PRs for every outdated dependency, and even groups them intelligently. The hosted version from Mend.io (formerly WhiteSource) works fine if you’re on GitHub and trust a third-party app reading all your code. But if you’re running GitLab on-prem, Gitea, or just don’t want a cloud service touching your private repos, the self-hosted path is the right call.
The good news: self-hosting Renovate is genuinely well-supported. The project is open source, the Docker image is maintained, and it runs as a one-shot CLI — no daemon, no sidecar, just a job you trigger on a schedule.
Official repository: https://github.com/renovatebot/renovate
How Renovate Actually Works
Before you set anything up, internalize this: Renovate is stateless. It doesn’t run as a server. You trigger it, it reads repos, opens/updates PRs, then exits. All state lives in the PRs themselves — Renovate reads its own previous comments to figure out what it already processed.
This is great for self-hosting. You run it via cron or a CI pipeline, and you’re done. No persistent process to babysit, no database to back up.
The configuration hierarchy matters:
- Global config (
config.jsor env vars) — controls which repos to scan, platform settings, token auth renovate.jsonin each repo — per-repo rules, package managers, schedules, grouping
What You Actually Need
- Docker (or Node.js 20+ if you hate Docker for some reason)
- A platform token with permissions to open PRs
- A place to run the container on a schedule (cron, GitLab CI, Kubernetes CronJob)
That’s it. No special database, no persistent volume, nothing exotic.
Step 1: Create the Bot Account
Renovate will open PRs as a specific user. Create a dedicated bot account on your platform — don’t use your personal account or a shared service account. The account needs:
- GitLab:
Developeror higher on every repo it should touch. Or add it to a group. - GitHub/GHE: Write access to repos, plus the ability to open PRs.
- Gitea: Write access similarly.
Generate a personal access token for this account. On GitLab, scope it to api. Keep this token — you’ll use it in a moment.
Step 2: Write the Global Config
Renovate’s global config is a JavaScript file (yes, JS, not JSON — this matters). Create a directory for it:
mkdir -p /opt/renovate
Create /opt/renovate/config.js:
module.exports = {
// The platform you're running against
platform: "gitlab", // or "github", "gitea", "bitbucket-server"
endpoint: "https://gitlab.yourcompany.com/api/v4/", // omit for github.com
token: process.env.RENOVATE_TOKEN,
// Which repos to scan — use "autodiscover" or list them explicitly
// autodiscover: true, // scan everything the bot account can see
// autodiscoverFilter: ["mygroup/*"], // narrow it down
repositories: [
"mygroup/frontend",
"mygroup/backend",
"mygroup/infra",
],
// Log level — use "debug" when something breaks, "info" for production
logLevel: "info",
// Renovate opens a lot of PRs. Rate-limit so you don't get spammed.
prHourlyLimit: 5,
prConcurrentLimit: 10,
// Persistence for the dependency dashboard issue (optional but recommended)
// This creates a "Dependency Dashboard" issue in each repo for visibility
dependencyDashboard: true,
};
Gotcha: The endpoint trailing slash is mandatory on some platforms (GitLab specifically). Missing it causes cryptic 404 errors that will waste your afternoon.
Gotcha: Don’t put the token directly in this file if you’re committing it anywhere. Use process.env.RENOVATE_TOKEN and inject via environment variable at runtime.
Step 3: Run It With Docker
Test the setup manually before you schedule anything:
docker run --rm \
-e RENOVATE_TOKEN=your_token_here \
-v /opt/renovate/config.js:/usr/src/app/config.js:ro \
renovate/renovate:latest
The image is renovate/renovate on Docker Hub. Don’t use latest in production — pin to a specific version tag (e.g., 39.x) so a surprise update doesn’t break your pipeline at 3am.
You should see Renovate logging its way through each repo, opening PRs for outdated deps, or reporting nothing to do if everything is current.
Step 4: Per-Repo Config (renovate.json)
Drop a renovate.json in the root of each repo. If it’s missing, Renovate still runs with defaults — but you lose control over grouping, schedules, and automerge.
A solid baseline:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"schedule": ["before 8am on Monday"],
"timezone": "Europe/Moscow",
"prCreation": "not-pending",
"labels": ["dependencies"],
"packageRules": [
{
"description": "Group all non-major npm updates together",
"matchPackagePatterns": ["*"],
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "npm non-major dependencies",
"automerge": false
},
{
"description": "Automerge patch updates for trusted packages",
"matchPackageNames": ["lodash", "axios"],
"matchUpdateTypes": ["patch"],
"automerge": true
},
{
"description": "Pin Docker base image digests",
"matchManagers": ["dockerfile"],
"pinDigests": true
}
]
}
The config:recommended preset is a sensible starting point — it sets up semantic commit messages, groups monorepo packages, and handles a few common edge cases. Read what it actually includes before you override things blindly: https://docs.renovatebot.com/presets-config/#configrecommended
Gotcha: Automerge sounds appealing but bites you eventually. Enable it only for patch updates on packages you trust and only after your CI is solid enough to catch regressions automatically. Major version automerge is almost always a mistake.
Step 5: Schedule It
Option A — System Cron
Simple and effective for a single machine:
crontab -e
# Run Renovate every day at 7am
0 7 * * * docker run --rm \
-e RENOVATE_TOKEN=your_token \
-v /opt/renovate/config.js:/usr/src/app/config.js:ro \
renovate/renovate:39 >> /var/log/renovate.log 2>&1
Redirect output to a log file. You’ll want it when something goes wrong.
Option B — GitLab CI Pipeline (recommended for GitLab users)
If you’re already on GitLab, a scheduled pipeline is cleaner than a cron job on some random VM:
# .gitlab-ci.yml in a dedicated "renovate-runner" repo
renovate:
image: renovate/renovate:39
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
variables:
RENOVATE_TOKEN: $RENOVATE_TOKEN # stored in GitLab CI/CD variables
LOG_LEVEL: "info"
script:
- renovate
artifacts:
when: always
paths:
- renovate-log.txt
expire_in: 3 days
Then create a pipeline schedule in GitLab (Settings → CI/CD → Schedules) to run this daily. The token goes into a masked CI/CD variable, never in the repo.
Option C — Kubernetes CronJob
For those already running k8s:
apiVersion: batch/v1
kind: CronJob
metadata:
name: renovate
namespace: tools
spec:
schedule: "0 7 * * *"
concurrencyPolicy: Forbid # don't stack if a run takes too long
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: renovate
image: renovate/renovate:39
env:
- name: RENOVATE_TOKEN
valueFrom:
secretKeyRef:
name: renovate-secret
key: token
- name: LOG_LEVEL
value: "info"
volumeMounts:
- name: config
mountPath: /usr/src/app/config.js
subPath: config.js
readOnly: true
volumes:
- name: config
configMap:
name: renovate-config
Create the secret and configmap separately — don’t bake them into this YAML.
Useful Config Patterns
Ignore specific packages
{
"packageRules": [
{
"matchPackageNames": ["some-legacy-package"],
"enabled": false
}
]
}
Handle monorepos
If your repo has multiple package.json files in subdirectories, Renovate handles this automatically. But you can be explicit:
{
"enabledManagers": ["npm"],
"packageFilePattern": ["**/package.json"]
}
Pin exact versions vs ranges
{
"rangeStrategy": "pin" // or "bump", "replace", "widen"
}
pin converts ranges to exact versions. Some teams love this for reproducibility; others hate it because it means more noise on every update. bump is usually the pragmatic middle ground — it bumps the version in your existing range constraint rather than pinning.
Security-only updates
For repos where you only want vulnerability-driven updates, not routine version bumps:
{
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["major", "minor", "patch"],
"enabled": false
},
{
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["patch"],
"vulnerabilityAlerts": {
"enabled": true
}
}
]
}
Renovate integrates with the GitHub Advisory Database and OSV. Even on self-hosted setups it can pull vulnerability data — configure vulnerabilityAlertsEnabled: true in the global config.
Gotchas Worth Knowing
Token permissions are the #1 source of silent failures. If the bot account doesn’t have permission to open a PR against a protected branch, Renovate logs a warning and moves on. You won’t get an error — just no PR. Check your branch protection rules if PRs aren’t appearing.
GitLab’s merge request approval rules. If your project requires approvals before merge, Renovate can technically open MRs but they’ll sit there waiting for manual approval. That’s fine — automerge won’t trigger unless the approval requirement is waived for the bot account, or you set up an approval rule that the bot satisfies. Plan for this before you enable automerge.
Rate limits. Both GitHub and GitLab have API rate limits. If Renovate is scanning 50+ repos, it will hit them. The prHourlyLimit and prConcurrentLimit settings in global config exist for this reason. Don’t remove them.
Dependency Dashboard issue noise. The Dependency Dashboard feature creates and continually updates an issue in each repo. Some teams find this useful. Others find it annoying. It’s opt-in — dependencyDashboard: false in renovate.json kills it per-repo.
Version pinning the Renovate image itself. You’re using Renovate to manage versions. Use it to manage its own Docker image version too. Add the runner repo to Renovate’s repo list. Yes, this creates a mild circularity, but it works fine in practice and means you’re never running a two-year-old Renovate.
Custom managers for internal registries. If your team uses a private npm registry, PyPI mirror, or internal Helm chart repo, you need to configure registryUrls and potentially hostRules for authentication. The docs cover this well but it’s easy to miss when you’re first setting things up.
Production Checklist
Before you call this done:
- Bot account uses a dedicated token, not someone’s personal token
- Token stored as an env variable or secret, never in
config.js - Renovate image pinned to a specific major version tag
-
prConcurrentLimitandprHourlyLimitset in global config - At least one repo has a
renovate.jsonwith a tested schedule - Logs go somewhere you can read them (file, CI artifacts, stdout to a log aggregator)
- You’ve manually verified a PR was opened and looks correct before trusting automerge
- The cron/schedule is set to a time when your CI pipeline is likely idle (avoid peak hours)
Debugging
When something isn’t working, the fastest path is LOG_LEVEL=debug:
docker run --rm \
-e RENOVATE_TOKEN=your_token \
-e LOG_LEVEL=debug \
-v /opt/renovate/config.js:/usr/src/app/config.js:ro \
renovate/renovate:39 2>&1 | grep -E "(ERROR|WARN|INFO|repo)"
Debug output is verbose. Pipe it through grep for the parts you care about. Look for authentication errors and 403/404 responses — those cover 80% of initial setup failures.
The Renovate team also maintains a config validator: renovate-config-validator renovate.json. Run it before committing a new config to avoid cryptic runtime errors.
Running Renovate self-hosted takes about an hour to get right the first time — mostly wrestling with token permissions and figuring out your preferred packageRules setup. After that, it runs invisibly and your dependency PRs just show up on schedule. That’s the deal: a bit of upfront config work in exchange for never manually checking changelogs again.