# Cloudflare Gateway Pi-hole Scripts: ad blocking with no server to run

> CGPS is a MIT licensed set of Node scripts that turn your own Cloudflare Zero Trust account into a Pi-hole style ad and tracker blocker, by uploading blocklists to Cloudflare Gateway lists and creating the firewall rule that applies them. It suits a home or small office network that already has Cloudflare, and it is a poor fit if you need the fine grained per client filtering or the local dashboard that Pi-hole gives you.

**mrrfv/cloudflare-gateway-pihole-scripts** — Use Cloudflare Gateway DNS/VPN to block ads, malware and tracking domains - free alternative to NextDNS, Pi-hole and AdGuard

- Repository: https://github.com/mrrfv/cloudflare-gateway-pihole-scripts
- Stars: 839 · Forks: 1,547
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/mrrfv-cloudflare-gateway-pihole-scripts

## The Pi-hole experience, hosted where the DNS already is

The premise of this project is a substitution. Cloudflare Gateway allows you to create custom rules to filter HTTP, DNS and network traffic based on your firewall policies, and this is a collection of scripts that can be used to get a similar experience as if you were using Pi-hole, but with Cloudflare Gateway.

The benefit the README names is the one that decides most adoptions: no servers to maintain or need to buy a Raspberry Pi.

A Pi-hole is a box you run, that answers DNS queries for your network, that you point your router at, and that you then keep alive, update and patch. A NextDNS account is the hosted equivalent, and so is Cloudflare Zero Trust for anyone who already has a Cloudflare domain and DNS. What this repository adds is the missing piece in that arrangement: the lists.

Cloudflare Gateway has the concept of lists, which are collections of indicators you can reference from a firewall rule. It does not, on its own, know about the community blocklists that people expect from a Pi-hole. This project takes the standard blocklists, cleans them up, and uploads them as Cloudflare Gateway lists, then creates the rule that blocks traffic matching them.

The audience is a home network or a small office that already has Cloudflare Zero Trust, and the alternative it positions against is running your own filtering box. The repository topics name the comparison directly, listing adblock, adblocking, anti-tracking, antimalware, cloudflare-gateway, cloudflare-zero-trust, nextdns, pihole, privacy, security and vpn.

The project is MIT licensed, written in JavaScript for Node, and the last push to the repository was on 2026-10-17. There are no GitHub releases, so again you take a commit.

## Five scripts, and a create and delete pair for each step

The scripts section lists what is in the box, and the naming makes the workflow obvious.

cf_list_create.js takes a blocklist.txt file containing domains and creates lists in Cloudflare Gateway. cf_list_delete.js deletes all lists created by CGPS from Cloudflare Gateway, which the README says is useful for subsequent runs. cf_gateway_rule_create.js creates a Cloudflare Gateway rule to block all traffic if it matches the lists created by CGPS, and cf_gateway_rule_delete.js removes that rule, also useful for subsequent runs. download_lists.js initiates blocklist and whitelist download.

So the update cycle is delete, create, delete, create, and the package.json turns that into npm scripts you can run without memorising file names.

```json
    "download": "node download_lists.js",
    "cloudflare-create": "npm run cloudflare-create:list && npm run cloudflare-create:rule",
    "cloudflare-delete": "npm run cloudflare-delete:rule && npm run cloudflare-delete:list",
    "cloudflare-refresh": "npm run download && npm run cloudflare-create"
```

The delete-before-create ordering is not incidental. Gateway lists are not patched in place, so a refreshed set of domains has to replace the old set, and the rule has to be removed and recreated rather than left pointing at a list that is being rebuilt.

The repository root also holds cf_defragment.js, which the package.json exposes as cloudflare-defragment. The README's feature list explains the cleanup behaviour in more detail: it automatically cleans up filter lists, removing duplicates, invalid domains, comments and more. That matters because the raw community lists are text files full of comments, blank lines and host entries that are not domains, and a Gateway list that fails to load is a silent failure.

The repository also ships two shell helpers, get_recommended_filters.sh and get_recommended_whitelist.sh, for pulling the recommended lists and allowlists on a machine that does not run Node.

## The 300,000 domain ceiling is the constraint that shapes everything

The prerequisites section contains the number that will determine whether this project works for you.

You need a file containing the domains you want to block, in the working directory, named blocklist.txt, and the limit is max 300,000 domains for the free plan. The optional CLOUDFLARE_LIST_ITEM_LIMIT setting is the maximum number of blocked domains allowed for your Cloudflare Zero Trust plan, defaulting to 300,000 and optional if you are using the free plan.

For scale, the recommended lists that download_lists.js fetches are OISD Small and AdAway, about 50,000 domains. So the default configuration uses about a sixth of the free plan allowance, which leaves room to add larger lists, and the cap is not the first thing that will stop you.

The prerequisites are otherwise light. Node.js installed on your machine, a Cloudflare Zero Trust account where the free plan is enough, and Cloudflare email, an API token with Zero Trust read and edit permissions, and an account ID. The API token is the part to be careful with, and the README points at an extended guide section on creating it.

Two optional inputs complete the picture. An allowlist.txt for domains you want to force unblocked, which the README describes as a way to prevent false positives and breakage, and a Discord or similar webhook URL for notifications.

The allowlist is not a nicety. A blocklist of a hundred thousand domains will include something you need, and the allowlist support is what makes the whole thing tolerable in daily use rather than a weekend experiment.

## Local runs, and why the README prefers them over Actions

The local procedure is seven steps, and step three is the one people skip.

Clone the repository, run npm install to install dependencies, copy .env.example to .env and fill in the values, and if you have not downloaded any filters yourself run the node download_lists.js command to download the recommended filter lists. Then run node cf_list_create.js to create the lists in Cloudflare Gateway, which the README warns will take a while, and run node cf_gateway_rule_create.js to create the firewall rule.

The .env file is where the credentials and switches live, and the example file shows the full set of knobs.

```bash
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_LIST_ITEM_LIMIT=300000
DRY_RUN=0
BLOCK_PAGE_ENABLED=0

# Experimental SNI based filtering. Set to 1 to enable.
BLOCK_BASED_ON_SNI=0
```

DRY_RUN is the one to set first. The dry runs section says you can set the DRY_RUN environment variable to 1, either in .env or the regular environment, to see whether your filter lists are valid without actually changing anything in your Cloudflare account. The package.json exposes the same idea as a script, where the dry script downloads the lists and then creates them with DRY_RUN set to 1.

Now the GitHub Actions route, because the README supports it and then immediately qualifies it. The scripts can be run using GitHub Actions so your filters will be automatically updated and pushed to Cloudflare Gateway, which is useful if you are using a frequently updated blocklist. But GitHub Actions was not intended to be used for this purpose, therefore the local options are recommended.

If you do use Actions, the README asks for a new empty, private repository, and says forking or public repositories are discouraged but supported, with the note that the script never leaks your API keys and that GitHub Actions secrets are automatically redacted from the logs, but it is better to be safe than sorry. There is no need to use the Sync fork button, because the Action downloads the latest code regardless of what is in your forked repository.

The workflow file you create is .github/workflows/main.yml with the contents of auto_update_github_action.yml, and the default schedule updates your filters every week at 3 AM UTC, changeable through the schedule property. Secrets are CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_LIST_ITEM_LIMIT, and optionally PING_URL and DISCORD_WEBHOOK_URL. Variables are ALLOWLIST_URLS, BLOCKLIST_URLS and BLOCK_PAGE_ENABLED.

One thing that catches people out: the variable names carry URLs, not file paths. The GitHub Action downloads the lists from the web, while the local run reads blocklist.txt from disk.

## SNI filtering needs WARP, and that changes the deployment

The DNS setup section has two paths, and the difference between them is not cosmetic.

The first is to go to the Cloudflare Zero Trust dashboard, navigate to Networks, then Resolvers and Proxies, then DNS locations, click the default location or create one, and configure your router or device based on the provided DNS addresses.

The second is to install the Cloudflare WARP client and log in to Zero Trust. This method proxies your traffic over Cloudflare servers, meaning it works similarly to a commercial VPN, and the README says you need to do this if you want to use the SNI based filtering feature, as it requires Cloudflare to inspect your raw traffic. The README adds that HTTPS remains encrypted if TLS decryption is disabled.

That requirement is the key. The experimental SNI based filtering works independently of DNS settings, preventing unauthorized or malicious DNS changes from bypassing the filter. In other words it defends against the case where something on the network points the client at a different resolver to slip past the blocklist.

The cost of that protection is that your traffic now traverses Cloudflare's proxy, which is a different privacy and performance profile from handing your resolver an address and nothing else. For a household that is fine. For anyone who was using this project precisely to avoid sending their browsing to a vendor, it is worth weighing explicitly, and the toggle defaults to off in the .env example, with the comment marking it experimental.

The block page switch sits next to it. BLOCK_PAGE_ENABLED controls whether a block page is shown when a host is blocked, and it is also listed as a GitHub Actions variable, so the behaviour can differ between the local run and the scheduled one if you set it in only one place.

## The default lists are ads and trackers, not malware

The malware blocking section is short and it is the most important caveat in the README.

The default filter lists are only optimized for ad and tracker blocking, because Cloudflare Zero Trust itself comes with much more advanced security features, and it is recommended that you create your own Cloudflare Gateway firewall policies that use those features on top of CGPS.

That is an important correction to the impression the topics list can give, since antimalware is one of them. The blocklists this project uploads are community domain lists aimed at advertising and tracking. The actual malware and threat protection comes from Cloudflare's own security products, and the sensible configuration is the community lists for ads plus the platform's security policies on top.

The other half of the design is the allowlist, and the README is clear about its purpose: to prevent false positives and breakage by forcing trusted domains to always be unblocked. A blocklist of tens of thousands of domains will contain sites you rely on, and the allowlist is where you put the exceptions. The ALLOWLIST_URLS and BLOCKLIST_URLS variables override the recommended lists with your own, one URL per line.

There is also an optional health check. After a successful run the script can send a ping request, ensuring continuous monitoring and alerting for the workflow execution, or message a Discord webhook with progress. For a scheduled job that runs unattended, a monitoring hook is what tells you the filters stopped updating six weeks ago.

## A container that runs the update on a schedule

The repository also ships a container setup, which is a natural fit for a job that does nothing but run on a timer.

The Dockerfile is short. It builds on the node alpine image with a lts tag, adds the project source, installs dependencies with npm ci, adds the tzdata package, makes the entrypoint script executable and hands the app directory to the node user. The entrypoint is the bundled script, and the default command runs crond in the foreground.

```dockerfile
ARG NODE_VERSION=lts

FROM docker.io/node:${NODE_VERSION}-alpine

WORKDIR /app

ADD . /app
```

That cron comment in the Dockerfile is the interesting part. The image ships a scheduler rather than a one shot job, so the container updates the lists on a schedule, and there is a docker-compose.yml.example in the repository for people who want to run it that way. The .env example has the matching settings, with TZ, CONTAINER_NAME, IMAGE_TAG and a CRON_SCHEDULE of 0 3 * * 1, which is the same weekly Monday 3 AM schedule as the GitHub Action.

The .env example also contains a small piece of housekeeping that suggests the author has been burned by a noisy dependency. DOTENV_CONFIG_QUIET is set to true, with a comment explaining that it disables dotenv tips and that you do not need to touch the value.

Finally, the housekeeping in the repository itself. There is a renovate.json for dependency updates, a .devcontainer directory, a .node-version file, and the package.json pins Node in its engines field at 24.18.1. The one runtime dependency is dotenv at 18.0.5, which tells you that the Cloudflare calls are made with the built in fetch rather than with an HTTP client library, and that everything else here is Node standard library and the Cloudflare API itself.

## Conclusion

CGPS suits anyone with a Cloudflare Zero Trust account who wants ad and tracker blocking across a whole network without running a Pi-hole, a Raspberry Pi or a VPS, since the entire blocklist lives in Cloudflare Gateway and the only local cost is a scheduled script run. It does not suit a household that wants per device policies, a local log of every query, or a UI to see what was blocked, because Cloudflare Gateway is the filter and the analytics view is Cloudflare's. Before you set it up, confirm your plan allows the number of domains you intend to push, since the free plan is capped at 300,000 and the default recommended lists are far smaller, decide whether you need SNI based filtering, which requires the WARP client rather than plain DNS, and read the DRY_RUN mode first, because the last push to the repository was on 2026-10-17 and the scripts are documented as in flux.

## FAQ

### What is Cloudflare Gateway Pi-hole Scripts?

It is a collection of MIT licensed Node scripts that create Cloudflare Gateway lists from a blocklist file and a Gateway rule that blocks traffic matching them, giving a Pi-hole like filtering experience through Cloudflare Zero Trust with no server to maintain.

### How many domains can I block on the free Cloudflare plan?

The README gives a maximum of 300,000 domains for the free plan, controlled by the CLOUDFLARE_LIST_ITEM_LIMIT setting which defaults to 300000. The recommended lists, OISD Small and AdAway, come to about 50,000 domains.

### How do I run the scripts for the first time?

Clone the repository, run npm install, copy .env.example to .env and fill in the values, run node download_lists.js to fetch the recommended lists, then run node cf_list_create.js and node cf_gateway_rule_create.js. Set DRY_RUN=1 first if you want to validate the lists without changing anything.

### Does SNI based filtering work without the WARP client?

No. The README says the SNI based filtering feature requires Cloudflare to inspect your raw traffic, so you need to install the Cloudflare WARP client and log in to Zero Trust, which proxies your traffic similarly to a commercial VPN. The feature is marked experimental and defaults to off.

### Does this project block malware as well as ads?

The default filter lists are only optimized for ad and tracker blocking. The README recommends creating your own Cloudflare Gateway firewall policies that use the advanced security features of Cloudflare Zero Trust on top of what these scripts configure.

## Sources

- [Issues](https://github.com/mrrfv/cloudflare-gateway-pihole-scripts/issues)
- [License: MIT](https://github.com/mrrfv/cloudflare-gateway-pihole-scripts/blob/main/LICENSE)
- [mrrfv/cloudflare-gateway-pihole-scripts on GitHub](https://github.com/mrrfv/cloudflare-gateway-pihole-scripts)
- [README](https://github.com/mrrfv/cloudflare-gateway-pihole-scripts/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mrrfv-cloudflare-gateway-pihole-scripts
