description: "Running a network of sites on Cloudflare's Free plan and a single origin server. Not a setup guide: an honest list of what went wrong and the checks that now prevent it."
I run 42 domains on Cloudflare's Free plan: tourism guides for my region in Greece, AI chat assistants, a couple of SaaS platforms and some client sites. Almost all of them point at one VPS. The rest are answered at the edge by Workers and never touch the server.
Cloudflare costs me nothing. What it costs is attention, because a free, powerful dashboard makes it very easy to end up with 42 zones configured 42 slightly different ways.
This isn't a setup guide. It's the list of things that actually broke, or quietly leaked, and the check I now run so they don't happen again.
Diagram: zones (proxied, DNS only, Worker routes, parked) pass through the Cloudflare edge (SSL modes, edge cache, WAF) to one origin VPS running nginx and Node services. Five numbered failure points are marked.
1. One grey cloud exposes everyone
Cloudflare's orange cloud hides your origin IP. The grey cloud ("DNS only") publishes it.
When I inventoried every zone, some sites were proxied and some weren't. Even within one family of six near-identical sites the split was three and three, and nobody could tell me whether that was deliberate.
That split matters more than it looks. All of those sites share the same IP. One DNS-only record reveals the origin for every proxied site on the same box, and the proxy protection on the other 41 is only as strong as that one grey cloud.
The quickest audit is to ask each domain who answered:
curl -sI https://example.gr | grep -i -E '^(server|cf-ray):'
A cf-ray header means the request went through Cloudflare. No cf-ray means you just talked to your origin directly.
The real fix goes further than flipping clouds to orange. An origin IP is discoverable from historical DNS anyway, so the server itself should only accept web traffic from Cloudflare's IP ranges. That's on my list, and it only becomes possible once no zone depends on a grey cloud.
2. Flexible SSL: a padlock with plain HTTP behind it
The same inventory found four live, proxied sites on the Flexible SSL mode.
Flexible means the visitor gets HTTPS to Cloudflare, and Cloudflare talks to your origin over plain HTTP. The browser shows a padlock. The second half of the trip is unencrypted. It works, it looks secure, and nothing ever warns you.
Other zones had drifted too: some on "Automatic" SSL mode, where Cloudflare picks for you and can change its mind, and six with Always Use HTTPS switched off.
3. Switching to Full (strict) took four sites down
The obvious fix for Flexible is Full (strict): Cloudflare connects to the origin over HTTPS and verifies its certificate. I switched the four sites over, and all four started returning a JSON {"error":"not found"}.
These were static sites. They have no JSON anywhere.
What happened: those domains had no server block on port 443 at all. Under Flexible that never mattered, because Cloudflare only ever knocked on port 80. Under Full (strict), Cloudflare knocked on 443, nginx found no matching server_name, and handed the request to its default server for that port. With no explicit default_server, that's simply the first listen 443 ssl block nginx loads. On my machine it belonged to an API subdomain running Express, which politely replied that / didn't exist.
I switched them back to Flexible. The rule I use now, before touching any SSL mode, is to ask the origin directly, bypassing Cloudflare:
curl -sk -o /dev/null -w '%{http_code}\n' \
--resolve example.gr:443:ORIGIN_IP https://example.gr/
200 means go ahead. 404 or 000 means there's nothing real on 443 yet, and switching will break the site.
The proper fix, in order:
- A Cloudflare Origin Certificate per domain (it can last up to 15 years, so no renewal cron to forget)
- A real 443 server block, plus a redirect from 80 to 443
-
nginx -t, reload, thecurl --resolvecheck again - Only then, Full (strict)
All eight hostnames came back with 200. One more thing I'd recommend: an explicit catch-all for unknown hostnames, so a missing block fails obviously instead of borrowing another site's backend:
server {
listen 443 ssl default_server;
ssl_reject_handshake on;
}
4. The change that didn't show up
I updated the footer on six sites in one go. Most showed the change straight away. One didn't, and I spent a while convinced the write had failed.
It hadn't. All six files had been written in the same second. That one zone was proxied and serving its HTML from Cloudflare's edge cache, so which site lagged wasn't random: it was predictable from how each zone was configured.
Checking the origin's copy of a file isn't enough on a proxied site. Look at what the edge is actually serving:
curl -sI https://example.gr | grep -i cf-cache-status
DYNAMIC means you're seeing the origin. HIT means you're seeing a copy, and you need a purge before you can judge the change.
5. nginx -t passed, and nothing changed
I added a location block, ran nginx -t, got "syntax is ok", reloaded, and the new endpoint returned 404.
The block had been pasted inside a section of the file that was commented out. nginx -t tells you the configuration parses. It says nothing about whether the lines you care about are active.
Even nginx -T, which dumps every loaded file, prints the comments along with everything else, so a careless grep will "find" a block that nginx is ignoring. The only proof that a change is live is a request that hits it:
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.gr/new-endpoint
Bonus: what a sweep found in the web roots
A read-only sweep across every vhost found two things that no dashboard ever showed me:
-
.git/configpublicly downloadable on seven sites - about 88 backup files (
index.html.bak-…and friends) sitting inside document roots
Both are now blocked by one shared snippet included in every server block, and the backups moved outside the web roots entirely. Mine looks roughly like this:
location ~ /\.(?!well-known) { return 404; }
location ~* \.(bak(-[^/]*)?|old|orig|swp|sql)$ { return 404; }
And a smaller one: static sites using try_files $uri $uri/ /index.html return 200 with the homepage for every wrong URL. That hides broken links from you and serves duplicate pages to search engines. For sites that aren't single-page apps, try_files $uri $uri/ =404 is the honest version.
The checklist I run per zone now
- Proxied, or deliberately DNS only? (
cf-ray) - SSL mode set explicitly, never "Automatic"
- A real 443 block on the origin (
curl --resolve) - Always Use HTTPS on
- On proxied zones:
cf-cache-statusbefore judging any change - No
.git, no backups, no soft-404s
None of this needed a paid plan. It needed looking at all 42 zones side by side, once, and writing down what each one actually does.
Do you run many zones on one origin? I'm especially curious how people handle locking the origin down to Cloudflare's IPs without losing direct access for maintenance.
Alexandros · Web Host Pro · Diakopto, Greece
Top comments (0)