Refreshing a page to see whether a price dropped, a restock landed, or a policy quietly changed is a chore a small home server can do for you.

What Changedetection.io Actually Does
Changedetection.io is an open-source web app that you host yourself. You give it a URL and a schedule, and it fetches the page at each interval, stores a snapshot, and compares it with the previous one. When the two differ, it flags the change, shows a side-by-side diff, and can send you a notification. It runs in a Docker container and is light enough for a spare mini PC, a NAS, or a small VPS.
The use cases are broader than price tracking. People point it at sold-out product pages, job boards, government notice pages, software changelogs, ticket release pages, and the terms-of-service documents that companies like to edit without announcement. Hosted monitoring services do similar work, but they tend to cap the number of watches on free tiers and keep your list of watched pages on their servers. Self-hosting removes both limits.
The most important decision in the tool is how it fetches a page. The default mode is a plain HTTP request, which is fast and cheap and works well for static pages. Many modern stores and dashboards, though, build their content with JavaScript after the page loads, so a plain request returns an empty shell and the app sees nothing to compare. For those sites you need the headless browser mode, which renders the page the way Chrome would before taking the snapshot.
You need Docker, Docker Compose, and a machine that stays on.
Installing It With Docker Compose
Create a folder for the project, such as changedetection, and add a docker-compose.yml file inside it. The main service needs only a handful of settings:
- image: ghcr.io/dgtlmoon/changedetection.io
- ports: 5000:5000, which exposes the web interface
- volumes: a named volume such as changedetection-data mapped to /datastore, so snapshots and settings survive container updates
- restart: unless-stopped, so it comes back after a reboot
- environment: BASE_URL set to the address you will use to reach the app, which matters later for notification links
If any of your targets rely on JavaScript, add a second service running a headless Chrome container and give the main service an environment variable named PLAYWRIGHT_DRIVER_URL that points to it, for example ws://browser-chrome:3000. The project documentation lists the currently recommended browser image, and it is worth checking there rather than copying an old snippet from a forum thread. Keep both services on the same Compose network so the hostname resolves. The browser container uses noticeably more memory than the main app, so a tiny VPS may feel the difference.

Start everything with docker compose up -d and open port 5000 on the host in a browser. The first thing to do is open Settings and set a password. Without one, anyone who can reach the port can view your watch list and change it, and that becomes a real problem the moment you put the app behind a reverse proxy on a public address.
Adding a watch takes one field: paste the URL, optionally add a tag such as “prices” or “jobs”, and save. Under the watch’s settings you pick the check interval and the fetch method, choosing the headless browser for JavaScript-heavy pages. A sensible starting point is a modest interval measured in hours rather than minutes. Hammering a small shop’s server every thirty seconds is rude, and it is also the fastest way to get your IP blocked.
Filters are what separate a useful watch from a noisy one. A whole web page contains timestamps, rotating banners, cookie notices, and “recommended for you” panels that change on every load, and each of those produces a false alert. The visual selector lets you click the part of the page you care about, such as the price block or the stock label, and the app builds a CSS or XPath filter for it. The “ignore text” option strips out strings you know will vary, while the “trigger text” option does the opposite and only alerts when a specific word appears, like “in stock” or “now hiring”. There is also a dedicated restock and price detection mode for product pages, which reads the structured data many stores embed. Spend ten minutes tuning the first watch properly and the rest become a copy-and-adjust job.
Getting Alerts You Will Actually Read
Notifications run through the Apprise library, which means a single URL format covers a long list of services. Discord, Telegram, ntfy, Slack, Matrix, and plain email are all supported, each with its own URL scheme such as discord:// or tgram:// followed by the tokens that service provides. Paste the URL into the notification field in Settings, use the test button to confirm a message arrives, and then decide whether it applies globally or only to specific watches or tags. Sending price alerts to a phone through ntfy while routing policy-page changes to email is a tidy way to keep urgent and archival alerts apart.

The default message includes the page title, the URL, and the diff, and you can edit the template if you want something terser. A short alert that says exactly what changed gets read, while a wall of raw HTML does not. One last detail is easy to miss: if BASE_URL is left blank, the “view diff” link inside each notification points to localhost, which does nothing when you tap it from your phone.
Frequently Asked Questions
Is Changedetection.io free to use?
Yes, the self-hosted version is open source and free. You only pay for the hardware or VPS it runs on.
Do I need a headless browser for every website?
No. Static pages work fine with the default plain HTTP fetch, while JavaScript-heavy sites such as many online stores need the headless Chrome option to render content before comparison.
How do I get notifications from Changedetection.io?
Add an Apprise-style URL for a service like Discord, Telegram, ntfy, or email in the notification settings. Use the test button to confirm delivery.
How often should I set the check interval?
Hours rather than minutes is a sensible default. Very frequent checks put needless load on the target site and can get your IP address blocked.





