Skip to Content

Production Guide: Deploy changedetection.io with Docker Compose + Caddy + Playwright on Ubuntu

Monitor website changes privately with TLS, browser-based checks, persistent storage, queues, backups, and clear alerting operations.

A small operations team often needs to know when a vendor status page, a pricing page, a documentation page, or a competitor landing page changes, but sending those checks through a third-party SaaS is not always acceptable. changedetection.io is a practical self-hosted answer: it stores watch rules locally, can render JavaScript-heavy pages with a browser worker, and can send alerts through email, webhooks, Discord, Slack, or other notification targets.

This guide mirrors the production pattern used across our recent Guides posts: a single Ubuntu host, Docker Compose for repeatable service definitions, Caddy for automatic HTTPS, persistent volumes, operational checks, and a recovery path you can rehearse before the first real incident. The deployment uses Playwright-compatible browser rendering for modern sites and Redis for queue reliability, which keeps interactive checks from blocking the main application.

Architecture and flow overview

The request path is intentionally simple. Users connect to https://watch.example.com, Caddy terminates TLS and proxies to the changedetection.io container, and changedetection.io stores rules under /datastore. When a watch needs full browser execution, it delegates the page load to the Playwright/browserless container over the private Docker network. Redis provides a durable queue backend for fetch workers, which helps when several watched pages change or timeout at once.

Only Caddy publishes ports 80 and 443. The application, Redis, and browser worker stay on the internal Compose network. Backups are file-level archives of the datastore, Compose file, Caddy configuration, and environment file. For a larger team, you can add SSO or an upstream access proxy later, but the base deployment keeps the moving parts easy to audit.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with at least 2 vCPU, 2 GB RAM, and 20 GB disk.
  • A DNS record such as watch.example.com pointing to the server.
  • Outbound HTTPS access from the server to the sites you monitor.
  • SMTP, webhook, or chat credentials for alerts after the first login.
  • A maintenance window to test restore, browser rendering, and notification delivery.

Step-by-step deployment

Start by installing Docker Engine, the Compose plugin, and a minimal firewall. Keep SSH open, then expose only HTTP and HTTPS for Caddy. This guide assumes a fresh host; if Docker is already installed, still confirm the Compose plugin version before continuing.

sudo apt update
sudo apt install -y ca-certificates curl gnupg ufw
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable

If the copy button is unavailable in your browser, select the command block and copy it manually.

Create the working directory and keep secrets out of the Compose file. The example uses .env for non-secret runtime values and a separate generated secret placeholder for teams that later add an upstream authentication layer or webhook signing. Replace watch.example.com with the confirmed DNS name before starting the stack.

sudo mkdir -p /opt/changedetection/{data,caddy,backups}
sudo chown -R $USER:$USER /opt/changedetection
cd /opt/changedetection
openssl rand -hex 32 > .secret-key
cat > .env <<'EOF'
DOMAIN=watch.example.com
BASE_URL=https://watch.example.com
TZ=UTC
EOF
chmod 600 .env .secret-key

If the copy button is unavailable in your browser, select the command block and copy it manually.

Now write the Compose file. The main service persists its datastore on the host. The browser worker runs separately so memory pressure or slow JavaScript pages do not crash the application process. Redis is private and append-only, providing a safer queue backend than a fully in-memory configuration.

cat > docker-compose.yml <<'EOF'
services:
  changedetection:
    image: dgtlmoon/changedetection.io:latest
    restart: unless-stopped
    depends_on:
      - playwright
      - redis
    environment:
      - BASE_URL=${BASE_URL}
      - TZ=${TZ}
      - PLAYWRIGHT_DRIVER_URL=ws://playwright:3000/?stealth=1&--disable-web-security=true
      - REDIS_URL=redis://redis:6379/0
      - HIDE_REFERER=true
      - FETCH_WORKERS=4
    volumes:
      - ./data:/datastore
    networks: [internal]

  playwright:
    image: browserless/chrome:latest
    restart: unless-stopped
    environment:
      - MAX_CONCURRENT_SESSIONS=4
      - CONNECTION_TIMEOUT=60000
      - DEFAULT_BLOCK_ADS=true
    shm_size: "1gb"
    networks: [internal]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --save 60 1 --appendonly yes
    volumes:
      - redis-data:/data
    networks: [internal]

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks: [internal]

networks:
  internal:
volumes:
  redis-data:
  caddy-data:
  caddy-config:
EOF

If the copy button is unavailable in your browser, select the command block and copy it manually.

Add the Caddy reverse proxy and start the deployment. Caddy will request and renew certificates automatically when DNS is correct and ports 80 and 443 are reachable. If certificate issuance fails, stop and fix DNS or firewall rules before configuring watches.

source .env
cat > caddy/Caddyfile <<EOF
${DOMAIN} {
  encode zstd gzip
  header {
    X-Content-Type-Options nosniff
    X-Frame-Options SAMEORIGIN
    Referrer-Policy no-referrer-when-downgrade
  }
  reverse_proxy changedetection:5000
}
EOF

docker compose pull
docker compose up -d
docker compose ps

If the copy button is unavailable in your browser, select the command block and copy it manually.

Configuration and secrets handling best practices

After the first login, create an administrator password immediately and record it in your team password manager. Keep notification secrets inside the application settings or a locked-down environment file, not in shell history or pasted runbooks. If you use webhook targets, prefer service-specific tokens with narrow permissions and rotate them when a teammate leaves.

Set sensible watch defaults before onboarding users. For JavaScript-heavy pages, use the browser fetch method and add delays only where the page really needs them. For static documentation or RSS-like pages, use the faster basic fetcher. Group watches by owner or business process so an alert makes it clear who should triage it. Avoid one global inbox for every alert; route security, pricing, compliance, and vendor status watches to different channels.

For noisy pages, use changedetection.io filters rather than disabling checks. CSS selectors, ignore text rules, and visual selectors can reduce false positives from timestamps, ads, cookie banners, and rotating recommendations. Treat each rule as production configuration: document why it exists and test it after every site redesign.

Verification checklist

Run these checks before inviting the team. You want to verify that the containers are healthy, TLS works externally, the page loads through Caddy, browser rendering can start, and Redis responds from inside the private network.

cd /opt/changedetection
docker compose ps
docker compose logs --tail=80 changedetection
curl -I https://watch.example.com
curl -fsS https://watch.example.com | grep -i changedetection || true
docker compose exec redis redis-cli ping

If the copy button is unavailable in your browser, select the command block and copy it manually.

  • Create a test watch against a harmless page you control and confirm the first snapshot completes.
  • Change the watched page, run a manual recheck, and confirm the diff is understandable.
  • Configure one notification target and send a test alert.
  • Create one browser-based watch against a JavaScript-heavy page to validate the Playwright worker.
  • Review docker compose logs for repeated timeouts, permission errors, or storage warnings.

Backups and recovery

The datastore contains the watch definitions, history, and application state. Back it up daily and retain enough versions to recover from an accidental bulk edit. The script below creates compressed archives and removes copies older than fourteen days. For regulated environments, ship the archive to object storage after creation and test restores in a separate VM.

cat > /opt/changedetection/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/changedetection
stamp=$(date -u +%Y%m%d-%H%M%S)
docker compose exec -T changedetection sh -c 'sync || true'
tar -czf "backups/changedetection-${stamp}.tgz" data docker-compose.yml caddy/Caddyfile .env
find backups -type f -name 'changedetection-*.tgz' -mtime +14 -delete
EOF
chmod +x /opt/changedetection/backup.sh
sudo tee /etc/cron.d/changedetection-backup >/dev/null <<'EOF'
17 2 * * * root /opt/changedetection/backup.sh >/var/log/changedetection-backup.log 2>&1
EOF
/opt/changedetection/backup.sh
ls -lh /opt/changedetection/backups | tail

If the copy button is unavailable in your browser, select the command block and copy it manually.

A backup is only useful if the team knows how to restore it. Practice the sequence below with a staging hostname or a temporary VM. The restore should bring back watches, settings, and recent snapshots without needing to rebuild rules from memory.

cd /opt/changedetection
docker compose down
sudo mv data data.broken.$(date -u +%s)
mkdir data
tar -xzf backups/changedetection-YYYYMMDD-HHMMSS.tgz data
docker compose up -d
docker compose logs --tail=80 changedetection

If the copy button is unavailable in your browser, select the command block and copy it manually.

Common issues and fixes

Caddy cannot issue a certificate

Check that the DNS A or AAAA record points to the server and that ports 80 and 443 are reachable from the internet. If a corporate firewall sits in front of the host, allow ACME HTTP validation or use a DNS validation workflow.

Browser checks are slow or fail under load

Increase server memory, reduce concurrent browser sessions, and reserve browser rendering for pages that actually require it. Many documentation pages work better with the basic fetcher plus a targeted CSS selector.

Every check reports a change

Look for timestamps, random IDs, rotating ads, CSRF tokens, or personalized page fragments. Add ignore rules or narrow the watched area to a stable selector. Do not silence alerts globally; fix the watch definition.

Notifications do not arrive

Send a test notification from the application, then inspect provider logs. For SMTP, verify host, port, TLS mode, username, sender address, and whether the provider requires an app password. For webhooks, confirm the target channel still exists and the token has not been rotated.

The datastore has permission errors

Confirm the host directory exists and is writable by the container. If you copied data from another host, preserve ownership or use a maintenance window to adjust permissions before starting the stack.

FAQ

Should I use changedetection.io for uptime monitoring?

Use it for content changes, not as your only uptime monitor. Pair it with a dedicated uptime tool when response time, status code, and availability SLOs matter.

How often should watches run?

Match frequency to business impact. Vendor status pages may run every few minutes, while competitor pages or documentation can run hourly or daily to reduce noise and load.

Do I need the Playwright worker?

Not for every site, but it is valuable for modern pages that render content client-side. Keeping it separate lets you scale or troubleshoot browser checks independently.

How do I keep alerts actionable?

Use selectors, ignore rules, labels, and dedicated notification channels. A concise alert routed to the right owner is better than a noisy global feed.

Can multiple teams share one instance?

Yes, but agree on naming, tags, notification ownership, and retention. For strong separation between customers or departments, deploy separate instances.

What should be included in backups?

Back up the datastore, Compose file, Caddy configuration, and environment file. Also document DNS, notification providers, and any external access-control layer.

How do I upgrade safely?

Take a fresh backup, read the release notes, pull the new images, restart during a quiet period, and manually recheck representative watches before closing the change.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Typesense with Docker Compose + Caddy + API Keys on Ubuntu
Run a fast private search API for documentation, product catalogs, or internal portals with TLS, backups, and operational checks.