DockerSelf-Hosting

changedetection.io with Docker: get alerts when a web page changes (Linux, Windows, Mac)

Before you start

  • 17 min read

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

changedetection.io is a free, self-hosted page watcher that keeps snapshots, shows you what changed, and sends an alert through Apprise; it is a good fit for text and JSON pages, while JavaScript pages need a second browser container.

changedetection.io official logo

Logo: changedetection.io official logo, from the changedetection.io website Use it to watch a price, a restock page, a documentation page, a government notice or a JSON API. It runs in Docker, stores its data in one folder, and sends alerts to ntfy, email or other services through Apprise. You pay for the server, not for a subscription.

VerdictDetail
Useful?Mostly. The core job is clear and the Docker route is simple; JavaScript pages add a large browser container and a broad capability.
Best forSelf-hosters who want to watch prices, restocks, docs, government pages or JSON APIs and who can run Docker.
Skip it ifYou need a zero-maintenance managed service, or you cannot give a browser container the capabilities it asks for.
Version + date0.70.1, released 5 November 2026.
Time neededAbout 20–30 minutes for the basic setup. This is an estimate; it was not timed in this job.
RAM / sizeNo official RAM minimum. Compressed downloads: 402 MB for the app and 513 MB for the browser (amd64). These are not RAM or disk measurements.
Ports5000 (Mac: 5050); 80/443 for HTTPS.
Official docsgithub.com/dgtlmoon/changedetection.io/wiki
Made bydgtlmoon and contributors.
LicenceApache-2.0.

How it fits together#

One container runs the scheduler, fetcher, diff and Apprise; a second browser container is only needed when the page must run JavaScript; snapshots live in ./data, which the app mounts at /datastore.

Flow diagram: you add a watch, the timer starts a check, the fetcher gets the page response, the app compares it with the last snapshot, builds an Apprise alert, and the alert arrives by ntfy or email

The plain fetcher reads HTML and JSON and does not run JavaScript. If a page builds its content in the browser, the app talks to the browser container through PLAYWRIGHT_DRIVER_URL. The browser is optional, but it is the part that asks for SYS_ADMIN.

Before you start#

You need a Linux, Windows or Mac machine with Docker. The project publishes amd64, arm64 and armv7 images, so a Raspberry Pi is possible; this guide was not run on one.

  • Linux: Docker Engine and the Compose plugin.
  • Windows: Docker Desktop.
  • Mac: Docker Desktop.
  • RAM: no official minimum is published. The browser container is the heavy part; the Playwright wiki reports memory use from 200 MB to several GB. Start with a few watches and watch docker stats.
  • Storage: the two compressed images are about 915 MB together. Docker unpacks images and snapshots grow, so leave more than that free. No official disk figure is published.

Check Docker first:

Run in terminalbash
docker --version
docker compose version
  • [ ] Docker and Docker Compose work.
  • [ ] Port 5000 is free. On a Mac, use 5050 because AirPlay can hold 5000.
  • [ ] You can create a project folder at ~/docker_volumes/changedetection.
  • [ ] For the Traefik section: a domain, a DNS record, and ports 80 and 443 open.
  • [ ] You have a backup of any existing ./data folder before you update.

Tested on 2026-11-05: Linux = not run, Windows = not run, Mac = not run. Checked against the official docs.

Step 1: Create the folder and Compose file#

The project folder holds compose.yaml and the data folder, so the app's data lands in ~/docker_volumes/changedetection/data.

Linux#

Run in terminalbash
mkdir -p "$HOME/docker_volumes/changedetection/data"
cd "$HOME/docker_volumes/changedetection"

Windows (PowerShell)#

Codepowershell
New-Item -ItemType Directory -Force "$HOME\docker_volumes\changedetection\data" | Out-Null
Set-Location "$HOME\docker_volumes\changedetection"

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
mkdir -p "$HOME/docker_volumes/changedetection/data"
cd "$HOME/docker_volumes/changedetection"

Create compose.yaml in that folder:

File contentyaml
services:
  changedetection:
    image: dgtlmoon/changedetection.io:0.70.1
    container_name: changedetection
    restart: unless-stopped
    ports:
      - "127.0.0.1:5000:5000"   # Mac: change to "127.0.0.1:5050:5000"
    volumes:
      - ./data:/datastore
    environment:
      PLAYWRIGHT_DRIVER_URL: "ws://browser-sockpuppet-chrome:3000"
      HIDE_REFERER: "true"
      DISABLE_VERSION_CHECK: "true"
      LLM_FEATURES_DISABLED: "true"
      USE_X_SETTINGS: "1"
      # BASE_URL: "https://app.example.com"
    depends_on:
      browser-sockpuppet-chrome:
        condition: service_started

  browser-sockpuppet-chrome:
    image: dgtlmoon/sockpuppetbrowser:latest
    container_name: browser-sockpuppet-chrome
    restart: unless-stopped
    cap_add:
      - SYS_ADMIN
    environment:
      SCREEN_WIDTH: "1920"
      SCREEN_HEIGHT: "1024"
      SCREEN_DEPTH: "16"
      MAX_CONCURRENT_CHROME_PROCESSES: "10"

The app image is pinned to 0.70.1. The browser image is latest because the project publishes only latest plus old 0.0.1–0.0.3 tags; pinning an old browser is usually worse than tracking the current one.

Check it worked#

Run in terminalbash
docker compose config

This should print the merged file without an error. If it complains about depends_on or condition, update Docker Compose.

Tip: On a Mac, change the port to 127.0.0.1:5050:5000 before the first start. AirPlay can bind port 5000, and the app then returns 403.

Step 2: Start it and add your first watch#

Start both containers, open the web interface, and add a watch you can test.

Linux#

Run in terminalbash
docker compose up -d
docker compose ps

Windows (PowerShell)#

Run in terminalpowershell
docker compose up -d
docker compose ps

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
docker compose up -d
docker compose ps

Open http://localhost:5000. On a Mac, open http://localhost:5050.

Add a watch: paste the URL, choose a check interval, and save it. Then run a check from the watch menu. The exact label changes between versions; look for the preview or recheck action.

Check it worked#

Run in terminalbash
docker compose logs --tail 100 changedetection

You should see a normal startup line and no Python traceback. docker compose ps should list both changedetection and browser-sockpuppet-chrome.

Potential issue: If the browser shows 403 on a Mac, another service owns port 5000. Change the host port to 5050 and open http://localhost:5050.

Tip: Start with a text page you control. A login wall or a JavaScript page will look like a failure when the plain fetcher cannot see the content.

Step 3: Use JavaScript pages#

The browser container is already in the Compose file; switch the watch to the Chrome/Playwright fetcher when the page needs JavaScript.

Linux#

Run in terminalbash
docker compose logs --tail 100 browser-sockpuppet-chrome
docker stats

Windows (PowerShell)#

Run in terminalpowershell
docker compose logs --tail 100 browser-sockpuppet-chrome
docker stats

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
docker compose logs --tail 100 browser-sockpuppet-chrome
docker stats

Open the watch, switch the fetcher to the Chrome/Playwright option, save, and run a check. If the page needs a login, add browser steps; the exact labels vary by version.

Check it worked#

The preview should show the rendered text, not an empty page or a cookie banner. The browser log should show activity, and docker stats should show the browser container using memory while it works.

Potential issue: The Playwright wiki says the browser can grow from 200 MB to several GB. Restart the browser container if memory keeps climbing.

Tip: If you do not need JavaScript, remove the browser service and the PLAYWRIGHT_DRIVER_URL line. You save the 513 MB download and you do not need to give any container SYS_ADMIN.

Step 4: Send alerts to ntfy#

Apprise handles notifications; for ntfy you need a topic and the right URL scheme.

Linux#

Use your own ntfy server, or a hosted endpoint you trust. In changedetection.io, open the notification settings and add a URL:

Codetext
ntfys://<YOUR_NTFY_HOST>/<YOUR_TOPIC>
ntfy://<YOUR_NTFY_HOST>/<YOUR_TOPIC>
ntfy://<YOUR_NTFY_HOST>:<PORT>/<YOUR_TOPIC>
ntfy://<YOUR_USER>:<YOUR_PASSWORD>@<YOUR_NTFY_HOST>/<YOUR_TOPIC>
ntfy://<YOUR_TOKEN>@<YOUR_NTFY_HOST>/<YOUR_TOPIC>

The topic is required. The body limit is 32,768 characters; ?format=markdown is supported, and the default body format is text. A short body such as Page changed: {current_snapshot} is easier to read and less likely to hit a limit.

Windows (PowerShell)#

The notification settings are in the web interface, so the URLs are the same. Check the app log after the test:

Run in terminalpowershell
docker compose logs --tail 100 changedetection

Mac (Terminal)#

Same URLs and the same log command as Linux:

Run in terminalbash
docker compose logs --tail 100 changedetection

Send a test notification and check the device or channel you configured.

Check it worked#

The test should arrive. If it does not, check the app log for the notification error, then check the ntfy topic and URL scheme.

Potential issue: An empty notification body fails on many targets. Put text in the body, keep it short, and check the target's own limit.

Tip: Use ntfys:// when your ntfy server has TLS. Use ntfy:// only for a local or trusted plain-HTTP endpoint.

Put it behind HTTPS with Traefik#

This section is for a server with a domain. If you only use the app at home, use the Caddy home-only route below, or stay on localhost.

With Traefik#

Create the shared proxy network if it does not exist. If it already exists, skip this command:

Run in terminalbash
docker network create proxy

Create compose.override.yaml next to compose.yaml:

File contentyaml
services:
  changedetection:
    environment:
      BASE_URL: "https://app.example.com"
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.changedetection.rule=Host(`app.example.com`)"
      - "traefik.http.routers.changedetection.entrypoints=https"
      - "traefik.http.routers.changedetection.tls=true"
      - "traefik.http.services.changedetection.loadbalancer.server.port=5000"

networks:
  proxy:
    external: true

Start with both files:

Run in terminalbash
docker compose -f compose.yaml -f compose.override.yaml up -d

The base file keeps the app on 127.0.0.1:5000; Traefik reaches it over the proxy network. Set BASE_URL to the public address, and keep USE_X_SETTINGS=1 so the app trusts the proxy headers. If your Traefik entrypoint is named websecure instead of https, change the entrypoints label to match.

Check it worked#

Run in terminalbash
curl -I https://app.example.com

You should get an HTTP 200 or a redirect to the login page. A 404 usually means the router rule or the network is wrong; a 502 means Traefik cannot reach the container.

Potential issue: Known issue #2053 can produce noisy 304 errors behind a reverse proxy. It is still open; see Troubleshooting.

Tip: BASE_URL must match the public URL exactly, including https:// and the subdomain. A mismatch causes redirects and broken links.

Without Traefik: Caddy#

Caddy terminates HTTPS and proxies to the app. The host install is the simplest route, especially for tls internal.

Public domain (Let's Encrypt)#

On Debian, Ubuntu or Raspbian, install the official package:

Run in terminalbash
sudo apt install --yes debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

On Windows, use Chocolatey or Scoop:

Run in terminalpowershell
choco install caddy
Run in terminalpowershell
scoop install caddy

On a Mac, use Homebrew:

Run in terminalbash
brew install caddy

Create a Caddyfile:

File contenttext
app.example.com {
    reverse_proxy localhost:5000
}

On a Mac, use localhost:5050. Run Caddy in the foreground with caddy run; for a service, use the package's systemd unit and caddy reload after edits. Ports 80 and 443 must be open, and Caddy gets a Let's Encrypt certificate automatically.

Run in terminalbash
caddy run

Leave that terminal open and check from a second terminal:

Run in terminalbash
curl -I https://app.example.com

Home-only with tls internal#

For a home network without a public domain, use Caddy's local certificate authority:

File contenttext
https://changedetection.localhost {
    tls internal
    reverse_proxy localhost:5000
}

Run Caddy, then trust its root certificate in another terminal:

Run in terminalbash
caddy run

In a second terminal:

Run in terminalbash
caddy trust

caddy trust may need sudo on Linux or macOS; on Windows, run it from a terminal that can write to the trust store. Then open https://changedetection.localhost. Caddy renews the short-lived leaf certificate automatically.

If Caddy runs in Docker, mount the conf folder at /etc/caddy (not a single Caddyfile) and point the reverse proxy at host.docker.internal:5000 on Docker Desktop, or use --network host on Linux. Use caddy:2.11.8; the 2.11.9 Docker tag returned 404 when this was checked.

For the home-only route, tls internal is enough on your own network.

Security and testing#

Set a password before you expose the app, keep the data folder private, and do not publish port 5000 directly.

  • Set a password in the settings before you open the app to anyone else.
  • If you forget it, reset it with the lock file:
Run in terminalbash
docker exec -t changedetection touch /datastore/removepassword.lock
docker restart changedetection
  • Keep the data folder private: chmod 700 data on Linux or macOS. On Windows, keep it under your user profile and do not share it.
  • Keep the app bound to 127.0.0.1. Only Traefik or Caddy should listen on 80 and 443.
  • If you put credentials in .env, run chmod 600 .env on Linux or macOS; on Windows, keep the file under your user profile.
  • Notification URLs can contain tokens. Do not paste them into screenshots or public issues.

Check it worked#

Run in terminalbash
docker compose ps
docker compose logs --tail 100 changedetection
curl -I https://app.example.com

docker compose ps should show both services. The log should have no traceback, and the HTTPS check should return 200 or a redirect.

Privacy: If you enable AI features, page diffs and extracted text go to the third-party provider you choose. The Compose file sets LLM_FEATURES_DISABLED=true to keep those features off. When you monitor a site, respect its terms, robots.txt, access policies and the law.

What users report#

Seven reports read: six complaints and one neutral, with no praise. The sample is small, and issue trackers attract complaints, so this is not a satisfaction rate.

ReportWhat happenedWhat to do
#3401macOS AirPlay held port 5000 and the app returned 403.Use 127.0.0.1:5050:5000 and open http://localhost:5050.
#44370.60.5 broke 18 JavaScript watches with "no filters were found"; a downgrade to 0.60.4 fixed it.Pin a tested tag (this guide uses 0.70.1); if the error appears, read the issue before changing the fetcher.
#2053304 handling caused reverse-proxy errors. It is still open.Set USE_X_SETTINGS=1 and BASE_URL; follow the issue.
#3548, #3531ntfy notifications contained HTML tags, or plain text arrived as HTML. Both are closed.Use a plain-text body and send a test.
#4539A request for HAR tracing; an AI suggested editing playwright.py. It is open.Do not patch the container; use supported logs or wait for the feature.
#4654The price preview worked, but the overview price was empty on 0.60.10 (before the current 0.70 line). The reporter closed it.Check the preview; report it if it persists.

No praise reports were read, so this guide does not claim that users love it. The core flow is documented, but the edge cases above are real.

Common mistakes#

MistakeWhy it happensDo this instead
Using :latest and getting a regression:latest moves with every stable release; 0.60.5 broke JavaScript watches.Pin dgtlmoon/changedetection.io:0.70.1; read release notes; keep the previous tag.
Mapping 5000:5000 on macOSAirPlay can bind port 5000; the app returns 403.Use 127.0.0.1:5050:5000 and open http://localhost:5050.
Expecting JavaScript pages to work with the plain fetcherThe built-in fetcher reads HTML; it does not run JavaScript.Add the browser container, set PLAYWRIGHT_DRIVER_URL, and choose the Chrome/Playwright fetcher.
Using json:// for a JSON POSTApprise's json:// is not a generic JSON POST.Use post:// or posts:// and `
Empty or too-long notification bodyEmpty bodies fail on many targets; snapshots and diffs can exceed service limits.Put text in the body; keep it short; check the target limit (ntfy: 32,768 characters).
Patching playwright.py because an AI suggested itIt is an open feature request, not a supported setting.Do not edit the container; use supported logs or wait for the feature.
Forgetting USE_X_SETTINGS=1 behind a proxyThe app ignores proxy headers without it.Set it, set BASE_URL, and use the right X-Forwarded-Prefix for a subpath.

Tip: Keep a copy of the previous app tag in a comment. If a new tag breaks a watch, you can change one line and restart.

Troubleshooting#

ProblemSolution
403 Forbidden on a MacPort 5000 is taken by AirPlay. Use 5050:5000 and open http://localhost:5050.
The preview is empty or the watch never changesThe page may need JavaScript, a login, or a different layout. Check the fetcher, add the browser, and run a preview.
ntfy shows HTML tagsUse a plain-text body and send a test. Issues #3548 and #3531 are closed; keep the body short.
Reverse-proxy errors or 304 noiseSet USE_X_SETTINGS=1 and BASE_URL; known issue #2053 is still open.
The browser container uses more and more memoryRestart it. The Playwright wiki reports 200 MB to several GB; watch docker stats.
You forgot the passworddocker exec -t changedetection touch /datastore/removepassword.lock, then docker restart changedetection.
A notification test failsThe body may be empty, the URL scheme may be wrong, or the target may have a size limit. Check the app log.

Warning, no filters were found, no change detection ran - Did the page change layout?#

This usually means the page returned a different layout, a login wall, or JavaScript content that the plain fetcher cannot render. Open the watch, run a preview, switch to the Chrome/Playwright fetcher, and check the browser log. If it started after an update, read issue #4437: 0.60.5 broke 18 JavaScript watches, and a downgrade to 0.60.4 fixed it. This guide pins 0.70.1.

Unsolicited response received on idle HTTP channel starting with "0\r\n\r\n"; err=<nil>#

This exact line comes from Go's HTTP client and can appear with reverse proxies. It does not by itself prove a check failed. If checks also fail, set USE_X_SETTINGS=1, check BASE_URL, and read issue #2053. Do not patch the container to silence it.

Known bugs#

  • #2053 (open): 304 handling can cause reverse-proxy errors. Keep USE_X_SETTINGS=1, check BASE_URL, and follow the issue.
  • #4437 (closed same day): 0.60.5 broke JavaScript watches. Pin a tested tag (this guide uses 0.70.1), or use 0.60.4 if you hit the same error on that line.
  • #4539 (open): HAR tracing is a feature request. An AI suggested editing playwright.py; that is not a supported setting.

Updates and backups#

Pull a new image only after reading the release notes, and back up the whole data folder while the app is stopped.

Update#

Run in terminalbash
cd "$HOME/docker_volumes/changedetection"
docker compose pull
docker compose up -d
docker compose ps

The app tag stays at 0.70.1 until you change it. The browser image uses latest; if a browser update breaks a page, pin 0.0.3 as a temporary fallback and report the problem.

Backup on Linux or Mac#

Create backup.sh:

File contentbash
#!/usr/bin/env bash
set -euo pipefail
cd "$HOME/docker_volumes/changedetection"
mkdir -p backups
docker compose stop changedetection
tar -czf "backups/changedetection-$(date +%F).tar.gz" data
docker compose start changedetection

Make it executable and run it once:

Run in terminalbash
chmod +x backup.sh
./backup.sh

Schedule it at 03:00 with cron:

File contenttext
0 3 * * * /home/<you>/docker_volumes/changedetection/backup.sh

Backup on Windows#

Create backup.ps1:

Codepowershell
$root = "$HOME\docker_volumes\changedetection"
Set-Location $root
New-Item -ItemType Directory -Force .\backups | Out-Null
docker compose stop changedetection
Compress-Archive -Path .\data -DestinationPath ".\backups\changedetection-$(Get-Date -Format yyyy-MM-dd).zip" -Force
docker compose start changedetection

Create a daily task in Task Scheduler for 03:00 that runs the script with PowerShell.

Restore test#

Stop the app before restoring; it writes to disk while running.

Run in terminalbash
cd "$HOME/docker_volumes/changedetection"
docker compose stop changedetection
tar -xzf backups/changedetection-YYYY-MM-DD.tar.gz
docker compose start changedetection

On Windows, use Expand-Archive -Path .\backups\changedetection-YYYY-MM-DD.zip -DestinationPath . -Force after stopping the app. Then open the UI and check that your watches are back.

Tip: Restore one backup into a test folder before you need it. A backup you have never restored is only a guess.

Performance and resources#

No RAM or CPU measurement was made in this job. The only hard numbers here are the compressed image downloads.

ImageCompressed download (amd64)
dgtlmoon/changedetection.io:0.70.1407 MB
dgtlmoon/sockpuppetbrowser:latest513 MB

Source: Docker Hub tag API, read 5 November 2026. Bytes divided by 1,000,000 and rounded. These are compressed downloads, not RAM use, disk use after unpacking, or running memory.

There is no official RAM minimum. The Playwright wiki reports the browser can grow from 200 MB to several GB; restart it if docker stats shows it climbing. If you only fetch text pages, the browser container is not used.

The official Compose file lists these tuning knobs:

  • FETCH_WORKERS=10: how many fetches run at once.
  • MINIMUM_SECONDS_RECHECK_TIME=3: the shortest recheck interval.
  • PAGE_WATCH_LIMIT: unset means unlimited; set it to cap the number of watches.
  • SCREENSHOT_MAX_HEIGHT=20000: limits screenshot height for tall pages.

Monitor with docker stats and docker compose logs --tail 100 changedetection. Disk use grows with snapshots, so keep the backup schedule and watch free space.

FAQ#

Is changedetection.io free? Yes. The project is Apache-2.0. A hosted plan exists, but this guide is about self-hosting.

Does it work with JavaScript pages? The plain fetcher does not run JavaScript. Add the browser container, set PLAYWRIGHT_DRIVER_URL, and choose the Chrome/Playwright fetcher.

Where is my data? In ./data inside ~/docker_volumes/changedetection, mounted at /datastore in the container. Back up the whole folder.

Do I need a domain? No. You can use http://localhost:5000 (Mac: 5050). For HTTPS on a public server, use Traefik or Caddy with a domain. For a home network, use Caddy's tls internal.

Can I use ntfy? Yes. Use ntfy:// or ntfys://; the topic is required, and the body limit is 32,768 characters.

Does it run on a Raspberry Pi? The project publishes arm64 and armv7 images, and the browser has an arm64 image. It was not tested on a Pi in this job; watch memory and use a small number of watches.

Sources#

Checked against the official docs on 2026-11-05. Not run by DIY Lab Hub: no container was started, and no RAM, CPU or notification delivery was measured.