CF Workers and DNS: a good solution for a homelab

Global network connecting cloud infrastructure

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.dev for 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