Introduction

If you’re self-hosting applications with Docker Compose, you’ve probably faced this question: how do I keep my container images up to date?

It’s a common challenge. You set up your stack, everything works great, and then… you forget about it. Months later, you realize you’re running outdated (and potentially vulnerable) images. You might not be able to just pull the latest version and restart your containers because of breaking changes.

And it gets more complicated with multi-container stacks. Images can have dependencies on each other, and updating one might break compatibility with another. For example, the application you’re running might not be ready to jump to the latest and greatest PostgreSQL release. You still need to be careful about these interdependencies.

Let’s explore the options available today, from the simplest to the most GitOps-oriented approach.

The Options

Option 1: The “Latest” Tag Approach

The simplest (and riskiest) approach: use the latest tag and pull new versions when restarting containers.

services:
  myapp:
    image: someimage:latest

Pros: Dead simple, no configuration needed.

Cons: You have no idea what version you’re running. An update might break everything. No audit trail. This is essentially hoping for the best.

Option 2: Watchtower

Watchtower has been the go-to solution for a long time. It runs as a container itself and automatically updates your other containers.

services:
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Watchtower uses Docker labels to control which containers get updated. You can include or exclude specific containers using the com.centurylinklabs.watchtower.enable label:

services:
  myapp:
    image: someimage:1.0
    labels:
      - "com.centurylinklabs.watchtower.enable=false"  # Exclude from updates

Pros: Set and forget, works well for simple setups, granular control via labels.

Cons: Still automatic updates without approval, limited rollback capabilities, requires modifying your compose files with labels.

Option 3: UI-Based Management Tools

There are several UI-based alternatives that give you more visibility and control:

  • What’s Up Docker (WUD) - Monitors and notifies about available updates
  • Portainer - Full container management with update capabilities
  • Dockge - Lightweight docker-compose management
  • Arcane - Modern container management
  • Dockhand - Advanced features including GitOps workflows

These tools range from basic monitoring to full-featured management. Dockhand in particular stands out for its GitOps capabilities, but I won’t go into too much detail here.

Option 4: Renovate

This brings us to Renovate. It’s not the most straightforward option, but it’s the tool that taught me the most and helped me acquire genuinely useful GitOps skills.

Why do I prefer it?

  • Pull request workflow: Updates come as PRs you can review before merging
  • Full visibility: You see exactly what’s changing
  • Rollback is trivial: Just revert the commit
  • CI/CD integration: Trigger pipelines on merge
  • Highly configurable: Fine-grained control over what gets updated and when

What is Renovate?

Renovate is a dependency update tool that automatically creates pull requests to keep your dependencies up to date. While it’s commonly used for npm, pip, or other package managers, it also works wonderfully with Docker Compose files.

But Docker images are just one use case. Renovate can also manage updates for:

  • Terraform modules and providers
  • Helm charts
  • Kubernetes manifests
  • Ansible roles and collections
  • GitHub Actions
  • And many more…

In this post, I’m focusing on the Docker Compose use case, but the same principles apply to all these other scenarios.

How Does It Work?

In a nutshell:

  1. You grant the Renovate app access to your Git repository (GitHub, GitLab, Bitbucket, etc.)
  2. You add a renovate.json5 configuration file to your repo
  3. Renovate scans your files, detects Docker images, and creates PRs when updates are available

It’s that simple to get started.

Here’s the complete workflow visualized:

graph LR
    A[New Image Available] --> B[Renovate Scans Repo]
    B --> C[PR Created]
    C --> D[Review & Merge]
    D --> E[CI/CD Deploys]
    E --> F[Container Updated]
  

Configuration

Here’s what my renovate.json5 configuration looks like:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "dependencyDashboard": false,
  "schedule": ["* 0-5 * * *"],
  "timezone": "Europe/Brussels",
  "packageRules": [
    {
      "matchManagers": ["docker-compose"],
      "matchUpdateTypes": ["digest"],
      "automerge": true,
      "automergeType": "pr",
      "automergeStrategy": "merge-commit",
      "platformAutomerge": false,
      "requiredStatusChecks": null
    }
  ]
}

Let me break down the key parts:

  • extends: ["config:recommended"]: Start with sensible defaults
  • schedule: Only check for updates between midnight and 5 AM (my timezone)
  • matchManagers: ["docker-compose"]: Target Docker Compose files specifically
  • matchUpdateTypes: ["digest"]: Only match digest updates (you can also use minor, major, patch)
  • automerge: Automatically merge certain low-risk updates

Fine-Tuning Your Configuration

You can be very specific about what gets updated:

  • Only allow minor version updates
  • Group related updates together
  • Set different schedules for different services
  • Require manual approval for major versions

The Renovate documentation has a comprehensive list of all available options.

How It Works in Practice

Once configured, Renovate will scan your repository and create pull requests when updates are available.

A typical PR will show you:

  • The current version and the new version
  • A link to the changelog (when available)
  • Any breaking changes or release notes

You then have several options depending on your update strategy:

  • Manual merge: Review each PR and merge when ready
  • Automerge: Let Renovate merge low-risk updates automatically
  • Scheduled merge: Queue updates to be merged during maintenance windows

Tips and Best Practices

Trigger CI/CD Pipelines

The real fun begins when you start triggering pipelines based on those commits or merges.

In my case, I use a GitHub Action that:

  1. Connects to the server where the app is deployed
  2. Pulls the latest changes from the repository
  3. Restarts the containers with docker compose up -d
  4. Verifies everything is up and running
  5. Rolls back automatically if something goes wrong and sends me an alert

This gives me fully automated updates with a safety net.

Grouping Updates

If you have many containers, you might want to group related updates to reduce PR noise. Renovate supports grouping by package name patterns, update type, or custom rules.

Scheduling

Don’t let Renovate create PRs during work hours. Schedule it for off-peak times so you can review updates when you’re ready.

Start with Manual Merges

I’d recommend starting with manual merges and gradually enabling automerge for services you trust (official images, well-maintained projects).

Conclusion

Is all of this completely overkill for a homelab? Absolutely.

But that’s kind of the point. By setting up this workflow, you’ll get familiar with important concepts:

  • GitOps principles: Your infrastructure state lives in Git
  • Pull request workflows: Changes are reviewed before being applied
  • CI/CD pipelines: Automation triggered by code changes
  • Rollback strategies: When things go wrong (and they will), recovery is straightforward

I’m certainly not a Renovate expert by any means, but I’ve learned a lot through this journey. The documentation is excellent, and the community is helpful.

If you’re tired of manually checking for updates or worried about Watchtower silently breaking things, give Renovate a try. Your future self will thank you.


Resources: