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.
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.
| Verdict | Detail |
|---|---|
| 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 for | Self-hosters who want to watch prices, restocks, docs, government pages or JSON APIs and who can run Docker. |
| Skip it if | You need a zero-maintenance managed service, or you cannot give a browser container the capabilities it asks for. |
| Version + date | 0.70.1, released 5 November 2026. |
| Time needed | About 20–30 minutes for the basic setup. This is an estimate; it was not timed in this job. |
| RAM / size | No official RAM minimum. Compressed downloads: 402 MB for the app and 513 MB for the browser (amd64). These are not RAM or disk measurements. |
| Ports | 5000 (Mac: 5050); 80/443 for HTTPS. |
| Official docs | github.com/dgtlmoon/changedetection.io/wiki |
| Made by | dgtlmoon and contributors. |
| Licence | Apache-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.
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:
docker --version docker compose version
- [ ] Docker and Docker Compose work.
- [ ] Port
5000is free. On a Mac, use5050because AirPlay can hold5000. - [ ] You can create a project folder at
~/docker_volumes/changedetection. - [ ] For the Traefik section: a domain, a DNS record, and ports
80and443open. - [ ] You have a backup of any existing
./datafolder 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#
mkdir -p "$HOME/docker_volumes/changedetection/data" cd "$HOME/docker_volumes/changedetection"
Windows (PowerShell)#
New-Item -ItemType Directory -Force "$HOME\docker_volumes\changedetection\data" | Out-Null Set-Location "$HOME\docker_volumes\changedetection"
Mac (Terminal)#
Same commands as Linux:
mkdir -p "$HOME/docker_volumes/changedetection/data" cd "$HOME/docker_volumes/changedetection"
Create compose.yaml in that folder:
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#
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:5000before 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#
docker compose up -d docker compose ps
Windows (PowerShell)#
docker compose up -d docker compose ps
Mac (Terminal)#
Same commands as Linux:
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#
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#
docker compose logs --tail 100 browser-sockpuppet-chrome docker stats
Windows (PowerShell)#
docker compose logs --tail 100 browser-sockpuppet-chrome docker stats
Mac (Terminal)#
Same commands as Linux:
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_URLline. You save the 513 MB download and you do not need to give any containerSYS_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:
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:
docker compose logs --tail 100 changedetection
Mac (Terminal)#
Same URLs and the same log command as Linux:
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. Usentfy://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:
docker network create proxy
Create compose.override.yaml next to compose.yaml:
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:
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#
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_URLmust match the public URL exactly, includinghttps://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:
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:
choco install caddy
scoop install caddy
On a Mac, use Homebrew:
brew install caddy
Create a Caddyfile:
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.
caddy run
Leave that terminal open and check from a second terminal:
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:
https://changedetection.localhost {
tls internal
reverse_proxy localhost:5000
}
Run Caddy, then trust its root certificate in another terminal:
caddy run
In a second terminal:
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:
docker exec -t changedetection touch /datastore/removepassword.lock docker restart changedetection
- Keep the data folder private:
chmod 700 dataon 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 on80and443. - If you put credentials in
.env, runchmod 600 .envon 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#
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.
| Report | What happened | What to do |
|---|---|---|
| #3401 | macOS AirPlay held port 5000 and the app returned 403. | Use 127.0.0.1:5050:5000 and open http://localhost:5050. |
| #4437 | 0.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. |
| #2053 | 304 handling caused reverse-proxy errors. It is still open. | Set USE_X_SETTINGS=1 and BASE_URL; follow the issue. |
| #3548, #3531 | ntfy notifications contained HTML tags, or plain text arrived as HTML. Both are closed. | Use a plain-text body and send a test. |
| #4539 | A 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. |
| #4654 | The 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#
| Mistake | Why it happens | Do 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 macOS | AirPlay 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 fetcher | The 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 POST | Apprise's json:// is not a generic JSON POST. | Use post:// or posts:// and ` |
| Empty or too-long notification body | Empty 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 it | It 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 proxy | The 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#
| Problem | Solution |
|---|---|
403 Forbidden on a Mac | Port 5000 is taken by AirPlay. Use 5050:5000 and open http://localhost:5050. |
| The preview is empty or the watch never changes | The page may need JavaScript, a login, or a different layout. Check the fetcher, add the browser, and run a preview. |
| ntfy shows HTML tags | Use a plain-text body and send a test. Issues #3548 and #3531 are closed; keep the body short. |
| Reverse-proxy errors or 304 noise | Set USE_X_SETTINGS=1 and BASE_URL; known issue #2053 is still open. |
| The browser container uses more and more memory | Restart it. The Playwright wiki reports 200 MB to several GB; watch docker stats. |
| You forgot the password | docker exec -t changedetection touch /datastore/removepassword.lock, then docker restart changedetection. |
| A notification test fails | The 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, checkBASE_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#
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:
#!/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:
chmod +x backup.sh ./backup.sh
Schedule it at 03:00 with cron:
0 3 * * * /home/<you>/docker_volumes/changedetection/backup.sh
Backup on Windows#
Create backup.ps1:
$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.
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.
| Image | Compressed download (amd64) |
|---|---|
dgtlmoon/changedetection.io:0.70.1 | 407 MB |
dgtlmoon/sockpuppetbrowser:latest | 513 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#
- README at tag 0.70.1 and docker-compose.yml at tag 0.70.1.
- Latest release 0.70.1 and repository metadata.
- Docker Hub: app tag 0.70.1 and browser tags.
- Official wiki: Playwright content fetcher, notification configuration notes, reverse proxy, restoring backups, password protection, Microsoft Windows.
- Apprise ntfy documentation.
- Caddy: install, command line,
tlsdirective. - Go issue #19895 for the
Unsolicited response received on idle HTTP channellog line. - User reports: issues #3401, #4437, #4539, #2053, #3548, #3531, #4654.
- Official homepage for the logo path; the marketing claims there are vendor claims, not independent evidence.
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.