The site loaded perfectly on my local machine. I'd open a browser, type localhost or the local IP, and there it was — full page, images, everything. But the moment I tried to hit it from my phone on mobile data, nothing. Connection refused. Timeout. ERR_NAME_NOT_RESOLVED. Pick your error.
If you're self-hosting on Linux and you're stuck at exactly this point, this is the post I wish existed when I was debugging it. I'm going to walk through every layer I checked, what was wrong at each one, and how I fixed it — in the order you should actually check them.
★ MY SETUP — WHAT YOU'RE WORKING WITH
Web server: Apache2
Network: FortiGate 60F → FortiSwitch 124D
DNS: Pi-hole on LAN
Public access: Cloudflare Tunnel (no port forwarding)
Domain: thelineman.ca via Porkbun, DNS managed by Cloudflare
The Cloudflare Tunnel approach means I'm not doing traditional port forwarding through my router at all. The tunnel punches out from my server to Cloudflare's edge — no inbound ports opened, home IP stays hidden. But that also means there are more moving parts that can go wrong.
★ LAYER 1 — IS APACHE ACTUALLY RUNNING?
Obvious, but start here. Apache running locally doesn't mean it's configured to serve anything useful.
Then confirm it's actually serving your files and not just the default Apache page:
If you're seeing the default Apache page, your virtual host config isn't pointing at your site files. Check your config:
★ LAYER 2 — PERMISSIONS
Apache runs as the www-data user. If your files are owned by your user account with restricted permissions, Apache can see the directory but can't read the files — so it serves a 403 Forbidden instead of your page.
★ LAYER 3 — LOCAL DNS VS PUBLIC DNS
This one got me. I'm running Pi-hole as my LAN DNS server. I had a local DNS entry pointing thelineman.ca to my server's LAN IP — which worked fine on my home network. But from the public internet, that local DNS record obviously doesn't exist.
The trap: testing your domain from a device on your own Wi-Fi is not the same as testing it from the public internet. Your local DNS resolves it to your LAN IP. Your phone on mobile data hits Cloudflare's public DNS and gets a completely different answer — or nothing at all if the public DNS isn't configured yet.
You can also check what public DNS sees for your domain:
★ LAYER 4 — CLOUDFLARE TUNNEL ISSUES
The tunnel is the most powerful part of this setup and also the most opaque when something goes wrong. Here's how to diagnose it systematically.
IS THE TUNNEL RUNNING?
The logs will tell you immediately if the tunnel can't reach Cloudflare's edge, if there's an auth problem, or if it's successfully connected. Watch them while you try to load the site.
IS THE TUNNEL ROUTING TO THE RIGHT PLACE?
Common mistakes here: pointing the tunnel at https://localhost:443 when Apache is serving plain HTTP on port 80, or having the wrong hostname in the ingress rule. Cloudflare handles the HTTPS termination at the edge — your tunnel only needs to talk to Apache over plain HTTP internally.
IS THE DNS RECORD POINTING TO THE TUNNEL?
If the DNS record exists but the cloud icon is grey (DNS only), Cloudflare isn't proxying the traffic through the tunnel — it's trying to connect directly to your server, which won't work without port forwarding.
★ LAYER 5 — SSL AND CERTIFICATE ISSUES
With Cloudflare Tunnel, SSL is handled at the Cloudflare edge automatically — you get a valid public certificate without doing anything. But there are two SSL-related issues that still catch people out.
MIXED CONTENT WARNINGS
If your HTML files have hardcoded http:// links to images or scripts, browsers will block them when the page loads over HTTPS. Check your HTML for any non-secure references and update them to relative paths or HTTPS.
CLOUDFLARE SSL MODE
★ LAYER 6 — FIREWALL ON THE SERVER ITSELF
Pop!_OS doesn't enable ufw by default, but if you've enabled it manually, Apache needs to be allowed through:
Also worth checking that your FortiGate isn't blocking outbound connections from the server — the Cloudflare Tunnel needs outbound access on ports 443 and 7844. It almost certainly is fine, but if you've applied restrictive outbound policies, check the FortiGate logs.
★ THE FULL TROUBLESHOOTING CHECKLIST
★ WHAT FINALLY FIXED IT FOR ME
In my case it was two things hitting at once. The permissions issue caused an intermittent 403 — some pages loaded, some didn't, depending on which files I'd uploaded and when. And I was testing from my home network the whole time, where Pi-hole was resolving the domain locally and masking the fact that the public DNS wasn't propagated yet.
The fix: chown -R www-data:www-data /var/www/html solved the 403. Switching to mobile data to test revealed the DNS propagation issue. Once Cloudflare's DNS finished propagating (took about 10 minutes), it all came together.
The lesson: always test from outside your own network. Everything that makes your LAN convenient for local testing — Pi-hole, local DNS overrides, LAN IP routing — actively hides problems that external users will hit. Mobile data is your best friend for self-hosting validation._