Changedetection.io is an open source service that visits web pages on a schedule and alerts you when their content changes. It is useful for price and stock tracking, release pages, public notices, job listings, or any page without an RSS feed. In this tutorial you will run Changedetection.io with Docker Compose on Ubuntu 24.04, add a headless Chrome for JavaScript-heavy sites, publish it over HTTPS, and configure filters and notifications so you only get alerts that matter.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Plan for 1 GB of RAM for Changedetection.io alone and 2 GB or more if you use the headless browser.
  • A non-root user with sudo privileges.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository.
  • A domain name with a DNS A record pointing to your server, for example changes.your_domain. Replace your_domain with your own domain throughout the guide.
  • Ports 80 and 443 open in your firewall.

Step 1 - Creating the Compose file

Create a directory for the project:

sudo mkdir -p /opt/changedetection
cd /opt/changedetection

Create the Compose file:

sudo nano /opt/changedetection/compose.yaml
services:
  changedetection:
    image: ghcr.io/dgtlmoon/changedetection.io:latest
    restart: unless-stopped
    environment:
      BASE_URL: https://changes.your_domain
      PLAYWRIGHT_DRIVER_URL: ws://sockpuppetbrowser:3000
      TZ: Europe/Madrid
    volumes:
      - datastore:/datastore
    ports:
      - "127.0.0.1:5000:5000"
    depends_on:
      - sockpuppetbrowser

  sockpuppetbrowser:
    image: dgtlmoon/sockpuppetbrowser:latest
    restart: unless-stopped
    cap_add:
      - SYS_ADMIN
    environment:
      SCREEN_WIDTH: 1920
      SCREEN_HEIGHT: 1024
      SCREEN_DEPTH: 16
      MAX_CONCURRENT_CHROME_PROCESSES: 4

volumes:
  datastore:

What each part does:

  • BASE_URL is the public address of your instance. Changedetection.io uses it to build links (for example, the link to the diff) inside notifications.
  • sockpuppetbrowser is the headless Chrome service recommended by the Changedetection.io project. PLAYWRIGHT_DRIVER_URL tells Changedetection.io where to find it. The SYS_ADMIN capability is needed by Chrome's sandbox.
  • MAX_CONCURRENT_CHROME_PROCESSES caps how many browser sessions run at once. Each one can use a few hundred MB of RAM, so keep it low on small servers.
  • The web interface is bound to 127.0.0.1 because Docker publishes ports around UFW. The reverse proxy in Step 3 is the only public entry point, and the browser service has no published port at all.

Set TZ to your own time zone.

Step 2 - Starting the containers

Pull the images and start the stack:

sudo docker compose up -d

Check that both services are running:

sudo docker compose ps
NAME                                 IMAGE                                        SERVICE             STATUS          PORTS
changedetection-changedetection-1    ghcr.io/dgtlmoon/changedetection.io:latest   changedetection     Up 15 seconds   127.0.0.1:5000->5000/tcp
changedetection-sockpuppetbrowser-1  dgtlmoon/sockpuppetbrowser:latest            sockpuppetbrowser   Up 16 seconds

Confirm that the application answers locally:

curl -sI http://127.0.0.1:5000/ | head -n 1
HTTP/1.1 200 OK

Step 3 - Publishing the interface over HTTPS with Caddy

Caddy gets and renews a Let's Encrypt certificate automatically. Install it from the Ubuntu repository:

sudo apt update
sudo apt install caddy

Replace the default site configuration:

sudo nano /etc/caddy/Caddyfile
changes.your_domain {
    reverse_proxy 127.0.0.1:5000
}

Open the web ports and reload Caddy:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo systemctl reload caddy

Open https://changes.your_domain in your browser. You should see the Changedetection.io watch list.

Step 4 - Setting a password

Changedetection.io has no authentication by default, so anyone who finds the URL can read and change your watches. Go to Settings > General, enter a password in the Password protection field and click Save. You are logged out and asked for the new password.

Step 5 - Adding your first watch

On the main page, paste a URL into the Add a new change detection watch field, optionally add a tag such as prices, and click Watch. The page is fetched right away and becomes the baseline for later comparisons.

Click Edit on the watch to adjust it. The most useful settings on the General tab are:

  • Time between check: how often the page is fetched. Hourly or daily is enough for most pages and keeps you from being rate limited by the site.
  • Fetch method: Basic fast Plaintext/HTTP Client downloads the HTML directly, which is fast and light. Playwright Chromium/Javascript renders the page in the headless browser from Step 1. Use it only for pages that load their content with JavaScript.

After the next check, click Preview to see exactly which text Changedetection.io extracted from the page. If the text you care about is missing, switch the fetch method to the browser.

Step 6 - Filtering the part of the page you care about

Without filters, every change on the page (dates, ads, "last updated" footers, rotating banners) triggers an alert. Open the Filters & Triggers tab of a watch to narrow it down.

Selecting elements

The CSS/JSONPath/JQ/XPath Filters field takes one selector per line. Only the matching elements are compared:

.product-price
#stock-status

Lines that start with // or xpath: are treated as XPath:

//table[@id='releases']//tr[1]

For pages that return JSON, use a JSONPath expression with the json: prefix:

json:$.data.price

The Visual Filter Selector tab lets you click an element on a screenshot of the page instead of writing a selector by hand. It needs the browser fetch method.

Ignoring noise and reacting to specific text

Other fields on the same tab refine what counts as a change:

  • Remove elements: CSS selectors to strip before comparison, for example header, footer, .cookie-banner.
  • Ignore text: lines that contain this text are ignored. Wrap a value in slashes to use a regular expression, for example /Last updated: .*/.
  • Trigger/wait for text: only report a change when this text is present, for example In stock.
  • Block change-detection while text matches: suppress changes while the page shows this text, for example Out of stock.

Save the watch and click Recheck to see the result of the new filter in Preview.

Step 7 - Configuring notifications

Changedetection.io sends notifications through Apprise, so each destination is configured as a single URL. Set the default destinations in Settings > Notifications. Individual watches can override them on their own Notifications tab.

Some common Apprise URLs:

ServiceNotification URL format
Email over SMTP (TLS)mailtos://user:password@your_domain?smtp=smtp.your_provider.com&from=changes@your_domain&to=you@your_domain
Discorddiscord://webhook_id/webhook_token
Telegramtgram://bot_token/chat_id
ntfyntfys://ntfy.your_domain/topic
Generic JSON webhookjsons://hooks.your_domain/changedetection

The notification title and body can use template tokens. A compact body looks like this:

{{watch_url}} changed.
Diff: {{diff_url}}

{{diff}}

Click Send test notification to confirm that the destination works before relying on it.

Step 8 - Using the API

The REST API is useful for adding many watches or integrating with other tools. Copy the key from Settings > API and export it in your shell:

export CD_API_KEY="your_api_key"

Create a watch:

curl -s -X POST https://changes.your_domain/api/v1/watch \
  -H "x-api-key: $CD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/pricing", "title": "Example pricing"}'
{"uuid": "a1b2c3d4-5e6f-7a8b-9c0d-e1f2a3b4c5d6"}

List watches with their UUIDs, last check time and last change time:

curl -s https://changes.your_domain/api/v1/watch -H "x-api-key: $CD_API_KEY"

Queue an immediate recheck of one watch:

curl -s "https://changes.your_domain/api/v1/watch/your_watch_uuid?recheck=1" -H "x-api-key: $CD_API_KEY"

To add a list of URLs in bulk, use Import in the top menu and paste one URL per line.

Troubleshooting

An alert fires on every check. The page contains changing content such as timestamps or session tokens. Check Preview or the diff, then add a CSS filter, a Remove elements selector or an Ignore text rule for that part.

The page shows no content or a "please enable JavaScript" message. Switch the watch's fetch method to Playwright Chromium/Javascript. If that fails, check the browser container:

sudo docker compose logs sockpuppetbrowser --tail 50

Checks fail with 403 or CAPTCHA pages. The site blocks automated clients. Increase the time between checks; if the site keeps blocking you, it does not want to be scraped and you should respect that.

High memory usage. Lower MAX_CONCURRENT_CHROME_PROCESSES and use the plain HTTP fetcher for every page that does not need JavaScript. Check current usage with sudo docker stats --no-stream.

Backing up and upgrading

All watches, history and settings live in the datastore volume. The Backups page in the top menu creates a downloadable ZIP of the data. To upgrade, pull the new images and recreate the containers:

cd /opt/changedetection
sudo docker compose pull
sudo docker compose up -d

Conclusion

You now have a private Changedetection.io instance over HTTPS, with a headless browser for dynamic pages, filters that ignore noise, and notifications sent to the channels you use. Next, group your watches with tags so each group notifies a different channel, try the restock and price detection mode for product pages, and add the instance to your server backups.