changedetection.io

Last year I spent three weeks refreshing a visa appointment website, because slots appeared without warning and vanished in minutes, and the refresh tab became a part-time job I was doing badly, waking at odd hours to reload a page that mostly said nothing had changed. The same pattern kept repeating across my life in smaller ways, a graphics card I wanted sitting at an absurd price, a festival releasing tickets with no announcement, a government portal updating a policy page exactly once with no newsletter, and the common thread was that the web had quietly removed RSS from most of itself while I still wanted to be told when things changed.

changedetection.io is the tool that retired the refresh tab, thirty-three thousand stars on GitHub, a self-hosted watcher that checks any page on a schedule you set, diffs the content against the last check, and fires a notification through whatever channel you already have, and the hosted version charges eight dollars ninety-nine a month for the privilege of running their code on their servers, while the identical open-source core runs on mine for nothing.

The mental model is one line, here is a URL, here is what I care about on that page, tell me when it changes. Adding a watch is pasting the URL, and the default mode diffs the whole page, which sounds noisy until you use the visual selector to click on exactly the element that matters, the price div or the availability badge or the table row with your city, and from then on the only changes that reach you are changes to that one thing. There are filters for ignoring text, triggers that fire only when specific words appear, conditional rules like alert me only when the number drops below a threshold, and a price tracking mode that reads product metadata so a watch becomes a proper price history with upper and lower bounds.

The compose file is two containers, because the second one is the difference between watching the real web and watching a museum of empty shells.

services:
  changedetection:
    image: ghcr.io/dgtlmoon/changedetection.io:latest
    restart: unless-stopped
    ports:
      - "5000:5000"
    volumes:
      - datastore:/datastore
    environment:
      PLAYWRIGHT_DRIVER_URL: ws://sockpuppetbrowser:3000

  sockpuppetbrowser:
    image: dgtlmoon/sockpuppetbrowser:latest
    restart: unless-stopped
    shm_size: "2gb"
    cap_add:
      - SYS_ADMIN
    environment:
      SCREEN_WIDTH: "1920"
      SCREEN_HEIGHT: "1024"
      SCREEN_DEPTH: "16"
      MAX_CONCURRENT_CHROME_PROFILES: "50"
      DEFAULT_STEALTH: "true"

volumes:
  datastore:

The main container does the checking and the diffing, and sockpuppetbrowser is a headless Chrome pool that renders pages with full JavaScript, which matters because half the modern web ships an empty div and fills it in the browser, so a plain HTTP fetch sees nothing and reports no change while the page quietly changed everything. Per-watch you pick the fetcher, plain HTTP for fast cheap checks of simple pages, Chrome for anything that renders, and browser steps let you script interactions before the check, filling a login form or clicking a date picker, which is how the visa appointment site got watched behind its login.

Notifications go wherever ntfy goes in my stack, one ntfy:// URL per watch or globally, and the alert can carry a screenshot of the changed page so I can see the slot appeared without opening anything. The scheduling is finer than I expected, checks every twenty seconds for the pages where minutes matter, hourly for price watching, and time-of-day limits in any timezone, so the festival page only gets hammered during ticket windows and sleeps otherwise.

The AI summaries were the surprise, because you can point the tool at any OpenAI-compatible endpoint including a local Ollama container, write the intent once in plain English like notify me only when the price drops below fifty thousand, and the model reads each diff and suppresses everything that fails the intent, so the notification reads price dropped from fifty-four to forty-eight instead of a wall of diff noise. It runs against the local model, which means the watching and the judging both happen inside the house.

My visa appointment eventually appeared at 6:40 in the morning and my phone buzzed before the page finished loading on my side, the graphics card got bought at a price I would never have caught manually, and the refresh tab is gone from my life along with the low hum of guilt that came with it. The web stopped telling people when things change, so now my server asks instead, thirty times an hour, patiently, and only interrupts me when the answer is yes.

If the internet owes you a price drop, subscribe to the newsletter.