CF Workers and DNS: a good solution for a homelab

Cloudflare Workers and DNS are a good solution for a homelab when we want to publish websites without exposing the home server as the first line of defense.
The simplest model looks like this:
Internet → Cloudflare Edge → Worker / static assets
Practical benefits:
- a public website can run without an origin server;
- proxied DNS records return Cloudflare anycast addresses instead of the origin IP;
- TLS, redirects, cache, DDoS protection, and selected WAF rules run at the Cloudflare Edge;
- DNS and security policy become auditable Infrastructure as Code;
- the environment can be quickly restored from a Git repository after a failure.
Clear ownership instead of two sources of truth
Both Wrangler and the Cloudflare provider used by OpenTofu can configure some Worker properties.
We use a clear split of responsibilities:
| Owner | Resources |
|---|---|
| Wrangler | Worker code, static assets, compatibility date, bindings, observability, and workers_dev |
| OpenTofu | zones, DNS records, Worker Custom Domains, redirects, cache, WAF Custom Rules, and rate limiting |
Wrangler deploys the application first, and OpenTofu then connects its stable service name to the hostname and manages the infrastructure.
IaC example
# main.tf
terraform {
required_version = ">= 1.12, < 2.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# variables.tf
variable "cloudflare_api_token" {
type = string
sensitive = true
}
variable "website_zone_id" {
type = string
}
variable "account_id" {
type = string
}
# workers_dns.tf
locals {
website = {
zone_id = var.website_zone_id
domain = "example.net"
worker = "example-website"
}
}
resource "cloudflare_workers_custom_domain" "apex" {
account_id = var.account_id
zone_id = local.website.zone_id
hostname = local.website.domain
service = local.website.worker
}
resource "cloudflare_dns_record" "www" {
zone_id = local.website.zone_id
name = "www"
content = local.website.domain
type = "CNAME"
ttl = 1
proxied = true
}
resource "cloudflare_page_rule" "redirect_www" {
zone_id = local.website.zone_id
target = "www.${local.website.domain}/*"
priority = 1
status = "active"
actions {
forwarding_url {
url = "https://${local.website.domain}/$1"
status_code = 301
}
}
}
An existing A or CNAME record with the same name can prevent the Custom Domain from being created.
Mail records work differently. MX, SPF, DKIM, and DMARC remain regular DNS resources, while mail CNAMEs and validation records usually need to stay DNS-only.
Cloudflare proxy only supports the appropriate records for HTTP/HTTPS traffic. We describe this boundary in our Cloudflare and OVH mail guide.
Wrangler manages the Worker
A static website can use a small wrangler.jsonc file:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "example-website",
"main": "./src/worker.ts",
"compatibility_date": "2026-08-18",
"assets": { "binding": "ASSETS", "directory": "./dist", "run_worker_first": false },
"workers_dev": false,
"observability": { "enabled": true }
}
workers_dev: false removes the additional public workers.dev endpoint when production should be available only under a custom domain. During testing, it can be temporarily enabled.
run_worker_first: false is a good default for a static website. Cloudflare can then serve a static asset without executing Worker code.
If specific requests require Worker logic before reading the asset, run_worker_first can be enabled deliberately. There is no reason to run the Worker for every asset when it is not needed.
A cautious workflow looks like this:
npx wrangler deploy --dry-run
npx wrangler deploy
tofu fmt -check -diff
tofu validate
tofu plan
tofu apply
Deploying the Worker before creating a new Custom Domain prevents OpenTofu from referring to a service that does not yet exist.
In CI, we first build and test, then deploy the Worker, and finally apply the approved infrastructure plan separately.
We describe this stack in more detail in Choosing technology for a blog and company website in 2026.
API tokens and minimum permissions
| Token | Permission | Use |
|---|---|---|
| Wrangler deployment | Account Workers Scripts: Edit |
deploy and update the Worker |
| OpenTofu base | Zone DNS: Edit |
manage DNS records |
| OpenTofu Custom Domain | Account Workers Scripts: Edit |
associate the domain with the Worker |
Add other permissions only when they correspond to resources that exist in the configuration:
| Optional OpenTofu resource | Permission |
|---|---|
| creating or managing zones | Account Zone: Edit |
| zone TLS and HTTPS settings | Zone Zone Settings: Edit |
| WAF Custom Rules | Zone WAF: Edit |
| cache rules | Zone Cache Settings: Edit |
Cloudflare defines Edit as CRUDL: create, read, update, delete, and list. Edit therefore includes Read/List where applicable; adding separate Read access for the same group does not increase access.
See Create API token and API token permissions for details.
Do not put secrets in .tf, tfvars, Wrangler configuration, shell history, or Git. Pass them from a secret manager or protected CI variables, for example as TF_VAR_cloudflare_api_token for OpenTofu and CLOUDFLARE_API_TOKEN for Wrangler.
OpenTofu state can contain sensitive data even when the terminal marks it as sensitive. A remote backend should be encrypted and protected with strict access control.
What is available for free?
The most important distinction is between two request paths:
static asset → no Worker invocation
dynamic request → Worker invocation
run_worker_first: true → Worker also runs before the asset
For the Workers Free plan:
- requests served directly as static assets are free and unlimited;
- a Worker has a limit of 100,000 requests per day;
- the CPU limit is 10 ms per Worker invocation;
- DNS, Universal SSL, and DDoS protection are also available on the Free plan;
- WAF, cache, redirects, and logs have their own limits depending on the current plan.
For a typical static site, run_worker_first: false is therefore a good starting point.
Limits and plan contents change. Before designing a solution around a specific number, check the current Workers pricing, Workers limits, and WAF Custom Rules availability.
OpenTofu and Wrangler are free tools, but they can create paid Cloudflare resources.
A small and precise WAF layer
A static website on a Worker should not receive requests for .env, .git, PHP, or WordPress paths.
WAF Custom Rules can reject such probes before the Worker runs:
resource "cloudflare_ruleset" "probe_block" {
zone_id = var.website_zone_id
name = "Block probes"
kind = "zone"
phase = "http_request_firewall_custom"
description = "Block paths absent from the static site"
rules = [{
action = "block"
enabled = true
description = "Block sensitive-file and legacy-CMS probes"
expression = "(http.request.uri.path contains ".env") or (http.request.uri.path contains "/.git") or (lower(http.request.uri.path) contains ".php") or (lower(http.request.uri.path) contains "/wp-")"
}]
}
This does not replace origin security. The edge WAF reduces unnecessary HTTP traffic, while the homelab firewall still protects the network layer.
Operational guidelines
- Pin the Cloudflare provider version and commit
.terraform.lock.hcl. - Store OpenTofu state remotely with locking, encryption, backups, and minimum access.
- Import existing DNS records before
tofu apply; otherwise OpenTofu may try to create duplicates. - Require review of
tofu plan, especially DNS record deletions and Worker Custom Domain replacements. - Keep mail records DNS-only and verify SPF, DKIM, and DMARC after changes.
- Disable
workers.devfor a production Worker with a custom domain unless that endpoint is intentionally used. - Where possible, use separate hostnames and tokens for staging.
The result is deliberately simple: Wrangler deploys the application, OpenTofu connects it to the domain and secures the edge, while the homelab firewall assumes that Cloudflare can be bypassed.
This separation makes a small environment easier to understand, restore, and secure.
Documentation
- Cloudflare Workers
- Cloudflare DNS
- Wrangler
- Cloudflare provider
- Cloudflare documentation for Terraform/OpenTofu rs.cloudflare.com/terraform/)