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

[ LAB SETUP ] OS: Pop!_OS 24.04 (Ubuntu-based)
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.

sudo systemctl status apache2 # Should show: active (running) # If it's not running: sudo systemctl start apache2 sudo systemctl enable apache2 # auto-start on boot

Then confirm it's actually serving your files and not just the default Apache page:

curl http://localhost # Should return your actual HTML, not the Apache2 Ubuntu Default Page

If you're seeing the default Apache page, your virtual host config isn't pointing at your site files. Check your config:

sudo nano /etc/apache2/sites-available/000-default.conf # Make sure DocumentRoot points to your actual site directory # DocumentRoot /var/www/html ← should be your site folder sudo apache2ctl configtest # Should return: Syntax OK sudo systemctl reload apache2

★ 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.

# Check ownership ls -la /var/www/html/ # Fix permissions sudo chown -R www-data:www-data /var/www/html/ sudo chmod -R 755 /var/www/html/
[ ⚠ COMMON MISTAKE ] If you upload files via SCP or SFTP as your own user, they'll be owned by you — not www-data. Apache will throw a 403. Run the chown command above every time you upload new files, or set up a deployment script that handles it automatically.

★ 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.

[ ALWAYS TEST FROM MOBILE DATA ] Turn off Wi-Fi on your phone completely. Test on mobile data only. That's the closest you can get to a real external user without using a VPN or a different network. If it works on mobile data, it works publicly.

You can also check what public DNS sees for your domain:

# Check what Cloudflare's public DNS resolves your domain to dig @1.1.1.1 thelineman.ca # Check what Google's public DNS sees dig @8.8.8.8 thelineman.ca # Should return your Cloudflare tunnel CNAME, not a LAN IP

★ 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?

sudo systemctl status cloudflared # Should show: active (running) # Check the tunnel logs live sudo journalctl -u cloudflared -f

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?

cat ~/.cloudflared/config.yml # Should show something like: # ingress: # - hostname: thelineman.ca # service: http://localhost:80 # - service: http_status:404

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?

# In Cloudflare dashboard: DNS → check for a CNAME record # Should be: thelineman.ca → YOUR-TUNNEL-ID.cfargotunnel.com # The orange cloud (proxied) should be ON

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

# In Cloudflare dashboard: # SSL/TLS → Overview → set to "Flexible" or "Full" # Flexible: Cloudflare to browser = HTTPS, Cloudflare to your server = HTTP # Full: both connections encrypted (requires cert on your server too) # For a home lab on HTTP internally, "Flexible" is fine to start

★ 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:

sudo ufw status # If active, make sure Apache is allowed: sudo ufw allow 'Apache Full' sudo ufw reload

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

[ SELF-HOSTED SITE NOT PUBLICLY REACHABLE — CHECKLIST ] Apache2 is running: systemctl status apache2 Apache is serving your files, not the default page: curl localhost File permissions are correct: chown -R www-data:www-data /var/www/html Virtual host DocumentRoot points to your site folder Apache config syntax is valid: apache2ctl configtest Testing from mobile data, not home Wi-Fi Public DNS resolves domain correctly: dig @1.1.1.1 yourdomain.com Cloudflare Tunnel is running: systemctl status cloudflared Tunnel config points to correct service and port Cloudflare DNS record exists with orange cloud (proxied) ON SSL mode in Cloudflare set to Flexible or Full No mixed content (http:// links) in HTML files ufw not blocking Apache (if ufw is enabled) FortiGate outbound policy allows port 443/7844 from server

★ 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._

[ ALSO ON THELINEMAN ] Running this site on a Fortinet-secured home lab. Here's the full network stack and what it actually cost to build.
FORTINET ON A BUDGET ▶