Self-Hosted Renovate: Automate Dependency Updates Without Giving Up Control

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:

  1. Global config (config.js or env vars) — controls which repos to scan, platform settings, token auth
  2. renovate.json in 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: Developer or 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.

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
  • prConcurrentLimit and prHourlyLimit set in global config
  • At least one repo has a renovate.json with 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.

Leave a comment

👁 Views: 2,289 · Unique visitors: 1,646