{"id":50486109,"url":"https://github.com/bartekmp/pihole-toggle-sync","last_synced_at":"2026-06-01T22:30:35.032Z","repository":{"id":351159869,"uuid":"1209807785","full_name":"bartekmp/pihole-toggle-sync","owner":"bartekmp","description":"Sync ad-blocking toggling for multiple Pi-hole instances simultaneously.","archived":false,"fork":false,"pushed_at":"2026-04-13T20:23:07.000Z","size":80,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-13T22:08:59.832Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bartekmp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-13T19:56:55.000Z","updated_at":"2026-04-13T20:23:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bartekmp/pihole-toggle-sync","commit_stats":null,"previous_names":["bartekmp/pihole-toggle-sync"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/bartekmp/pihole-toggle-sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bartekmp%2Fpihole-toggle-sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bartekmp%2Fpihole-toggle-sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bartekmp%2Fpihole-toggle-sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bartekmp%2Fpihole-toggle-sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bartekmp","download_url":"https://codeload.github.com/bartekmp/pihole-toggle-sync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bartekmp%2Fpihole-toggle-sync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797126,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T22:30:34.307Z","updated_at":"2026-06-01T22:30:35.027Z","avatar_url":"https://github.com/bartekmp.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"www/favicon.svg\" width=\"28\" height=\"28\" valign=\"middle\"\u003e Pi-hole Toggle Sync\n\nA lightweight web UI for toggling ad-blocking on one or more [Pi-hole](https://pi-hole.net/) instances simultaneously. Shows live stats per instance and syncs disable/enable actions across all of them at once.\n\n![License](https://img.shields.io/badge/license-MIT-blue)\n![Pi-hole](https://img.shields.io/badge/Pi--hole-v6%2B-red)\n![Build](https://github.com/bartekmp/pihole-toggle-sync/actions/workflows/publish.yml/badge.svg)\n\n![Pi-hole Toggle Sync screenshot](pts.png) ![Pi-hole Toggle Sync — blocking disabled](pts-disabled.png)\n\n---\n\n## Why run redundant Pi-hole instances?\n\nA single Pi-hole is a convenient network-wide ad blocker, but it also becomes a **single point of failure** for all DNS resolution on your network. If it goes down for a reboot, update, or hardware issue, every device on your network loses DNS — and with it, internet access.\n\nRunning two (or more) Pi-hole instances in parallel solves this:\n\n- **High availability** — configure your router to hand out both Pi-hole IPs as DNS servers. If one goes down, clients automatically fall back to the other with no interruption.\n- **Maintenance without downtime** — update or restart one instance while the other keeps serving queries.\n- **Load distribution** — DNS queries are spread across instances, reducing per-instance load on busy networks.\n- **Consistency** — both instances should have identical blocklists and settings. Keeping them in sync manually is error-prone; tools like [Nebula Sync](https://github.com/lovelaze/nebula-sync) or Pi-hole's built-in Teleporter can help.\n\nThe catch: **any action you take on one instance** — such as temporarily disabling blocking to let a site through — **must be mirrored on the other**, or traffic will simply be blocked by whichever instance the client happens to query. Doing this manually across two browser tabs is tedious and easy to forget.\n\nThat is what this tool is for.\n\n---\n\n## Features\n\n- Per-instance info cards showing queries today, blocked count, block rate, and blocklist size\n- Disable blocking across all instances at once — for 30s, 5m, 10m, a custom duration, or indefinitely\n- Live countdown timer with automatic re-enable\n- Per-instance status chips showing which hosts succeeded or failed\n- Instance cards refresh automatically every 30 seconds and after each toggle action\n- Material Design 3 UI with full dark mode support\n- Configurable entirely via environment variables — no build step needed\n- Multi-arch image: `amd64`, `arm64`, `armv7` (Raspberry Pi)\n\n## Requirements\n\n- Pi-hole **v6+** (uses the `/api/stats/summary` and `/api/dns/blocking` REST API endpoints)\n- Docker + Docker Compose\n\n## Quick start\n\n### Using the pre-built image from GHCR (recommended)\n\n```bash\ncurl -O https://raw.githubusercontent.com/bartekmp/pihole-toggle-sync/main/compose.yml\ncurl -O https://raw.githubusercontent.com/bartekmp/pihole-toggle-sync/main/.env.example\nmv .env.example .env\nnano .env\ndocker compose up -d\n```\n\nThe image is published to [GitHub Container Registry](https://ghcr.io/bartekmp/pihole-toggle-sync) and is publicly available without authentication.\n\n### Building from source\n\n```bash\ngit clone https://github.com/bartekmp/pihole-toggle-sync.git\ncd pihole-toggle-sync\ncp .env.example .env\nnano .env\ndocker compose up -d\n```\n\nThen open `http://\u003cyour-host\u003e:8087` in a browser.\n\n## Configuration\n\nAll configuration is done at **runtime** via environment variables — the image itself contains no hardcoded addresses. Set them in a `.env` file or directly in `compose.yml`.\n\n| Variable | Default | Description |\n|---|---|---|\n| `PH_HOSTS` | *(empty)* | Comma-separated list of Pi-hole base URLs. Embed a per-instance password with `http://:password@host` syntax. |\n| `PH_PASSWORD` | *(empty)* | Shared Pi-hole password used for instances without an embedded credential. Leave empty if none is set. |\n| `LISTEN_PORT` | `8087` | Host port to expose the UI on |\n\n### Example `.env`\n\n```env\n# Both instances share one password:\nPH_HOSTS=https://pihole.example.com,https://pihole2.example.com\nPH_PASSWORD=yourpassword\nLISTEN_PORT=8087\n\n# Or give pihole2 its own password:\n# PH_HOSTS=https://pihole.example.com,https://:differentpassword@pihole2.example.com\n# PH_PASSWORD=yourpassword\n```\n\n## CORS and reverse proxy\n\nAPI calls are made directly from the **browser**, not from the container. This means Pi-hole must be reachable from the browser and must respond with appropriate CORS headers for cross-origin POST requests (used when toggling blocking).\n\nThe recommended setup is a reverse proxy (e.g. Caddy) in front of each Pi-hole, which handles CORS and avoids Pi-hole ACL issues:\n\n```\npihole.example.com {\n    redir / /admin/ 301\n\n    @api path /api/*\n    header @api {\n        ?Access-Control-Allow-Origin *\n        ?Access-Control-Allow-Methods \"GET, POST, OPTIONS\"\n        ?Access-Control-Allow-Headers \"Content-Type, X-FTL-SID\"\n    }\n\n    @preflight {\n        method OPTIONS\n        path /api/*\n    }\n    respond @preflight 204\n\n    reverse_proxy http://127.0.0.1:8053\n}\n```\n\nIf you access Pi-hole directly by IP, ensure its webserver ACL allows your browser's LAN IP:\n\n```yaml\nFTLCONF_webserver_acl: \"+127.0.0.0/8,+192.168.0.0/24\"\n```\n\n## How it works\n\nThe UI is a single static HTML file served by nginx. On container start, an entrypoint script runs `envsubst` to substitute the `${PH_HOSTS}` and `${PH_PASSWORD}` placeholders in the HTML with runtime environment variable values. The browser then talks directly to each Pi-hole's REST API — there is no backend process.\n\n## Project structure\n\n```\npihole-toggle-sync/\n├── .github/\n│   └── workflows/\n│       └── publish.yml          # CI/CD: build \u0026 push to GHCR\n├── docker-entrypoint.d/\n│   └── 40-envsubst-html.sh      # Injects env vars into HTML at startup\n├── www/\n│   ├── index.html               # Single-file Material Design 3 UI\n│   └── favicon.svg              # App icon\n├── Dockerfile                   # nginx:alpine + www/ + entrypoint script\n├── nginx.conf                   # Static file server config\n├── compose.yml                  # Docker Compose service definition\n├── .env.example                 # Example environment file\n├── .gitignore\n├── LICENSE\n└── README.md\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbartekmp%2Fpihole-toggle-sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbartekmp%2Fpihole-toggle-sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbartekmp%2Fpihole-toggle-sync/lists"}