Subdomains End-to-End
Chapter 6 — Subdomains End-to-End
Previous chapters covered DNS, Cloudflare, tunnels, and virtual hosts as separate subjects. This chapter pulls them together: a subdomain only works when both the DNS layer and the server layer are configured correctly. Miss either one and the subdomain either doesn't resolve or resolves to the wrong place. This chapter is a practical integration guide — complete walkthroughs, common patterns, and the exact sequence to follow every time you add a new subdomain.
The Two-Part Requirement
A subdomain like blog.osztromok.com requires two completely separate systems to agree with each other. If either is missing, the subdomain fails — and the error messages are different depending on which part is absent.
blog.osztromok.com to Cloudflare's edge (via the tunnel CNAME or a direct A record).What goes wrong without it: Browser shows "This site can't be reached / DNS_PROBE_FINISHED_NXDOMAIN" — the domain doesn't resolve at all.
dig blog.osztromok.com returns NXDOMAIN (no record found).
ServerName blog.osztromok.com pointing to the correct document root (or ProxyPass to a backend).What goes wrong without it: DNS resolves correctly, browser reaches Apache, but Apache serves the default vhost (or returns 403/404). The right domain, the wrong content.
Common Subdomain Patterns
Scenario 1 — Static Subdomain End-to-End
Type: CNAME | Name:
blog | Target: osztromok.com | Proxy: orange cloud (proxied)If using Cloudflare Tunnel: instead of a DNS record, go to Zero Trust → Networks → Tunnels → your tunnel → Public Hostnames → Add a public hostname:
Subdomain:
blog · Domain: osztromok.com · Service Type: HTTP · URL: localhost:80The tunnel creates the CNAME automatically and routes traffic through. No separate DNS step needed.
Scenario 2 — Redirecting www to the Root Domain
Most sites pick one canonical form — either www.osztromok.com or osztromok.com — and redirect the other. This matters for SEO (duplicate content) and for user expectations. The standard modern approach is to use the apex (osztromok.com) as canonical and redirect www to it.
Method A — Apache redirect vhost (server-side)
www pointing to osztromok.com in Cloudflare, otherwise the redirect vhost is never reached. Apache can only redirect a request that actually arrives.
Method B — Cloudflare Redirect Rules (no Apache config needed)
If DNS is on Cloudflare, you can handle the www redirect entirely at the Cloudflare edge — the request never reaches your server. Go to: Rules → Redirect Rules → Create rule
- When: Hostname equals
www.osztromok.com - Then: Static redirect →
https://osztromok.com/(301 Permanent)
This is faster (redirect happens at Cloudflare's edge) and requires no Apache changes. Either method works — use the Cloudflare approach if you're already comfortable there, or the Apache approach if you want all routing logic on the server.
Wildcard Subdomains
A wildcard matches any subdomain that doesn't have a more specific record. It's one DNS record that covers anything.osztromok.com.
Wildcard DNS record in Cloudflare
| Type | Name | Target | Proxy |
|---|---|---|---|
| CNAME | * | osztromok.com | Orange cloud (proxied) |
This makes anything.osztromok.com resolve to Cloudflare's edge. Traffic still needs somewhere to land on the server.
*.osztromok.com matches foo.osztromok.com but not foo.bar.osztromok.com. If you need deeper subdomains, create explicit records for each.
Wildcard Apache vhost (catch-all for unknown subdomains)
z- prefix in the filename). Specific vhosts — blog.osztromok.com.conf, shop.osztromok.com.conf — match first. The wildcard only catches subdomains with no explicit vhost.
Scenario 3 — Reverse Proxy to a Backend App
When you have an application (Python/FastAPI, Node.js, anything) running on a local port (e.g. 8080), Apache can act as a reverse proxy: it accepts the subdomain request and forwards it to the app, then passes the response back to the visitor. The app doesn't need to know about Cloudflare or subdomains at all.
Subdomain Deployment Checklist
Use this every time you add a new subdomain. Work top-to-bottom — each step depends on the previous one.
sudo mkdir -p /var/www/subdomain.osztromok.com/public_html with correct ownership (philip:www-data) and permissions (755 dirs, 644 files).index.html in the document root, so a successful hit returns visible content rather than a 403/directory listing./etc/apache2/sites-available/subdomain.osztromok.com.conf with correct ServerName, DocumentRoot, Directory block (AllowOverride + Require all granted), and separate log files.sudo a2enmod rewrite for .htaccess rewrites; sudo a2enmod proxy proxy_http for reverse proxy; restart Apache after enabling modules.sudo a2ensite subdomain.osztromok.com.conf to create the symlink in sites-enabled.sudo apache2ctl configtest returns "Syntax OK" before reloading. Never skip this step.sudo systemctl reload apache2 — not restart.curl -H "Host: subdomain.osztromok.com" http://localhost returns the correct content. This confirms Apache is working before DNS is involved.subdomain → osztromok.com, OR new public hostname added to the Cloudflare Tunnel config.dig subdomain.osztromok.com +short returns a Cloudflare IP (not NXDOMAIN, not your home IP).curl https://subdomain.osztromok.com from outside the local network (or via mobile data) returns the correct content with 200 OK.curl -H "Host: osztromok.com" http://localhost still returns the correct main site content. Adding a new vhost should never disturb existing ones.Troubleshooting
dig subdomain.osztromok.com +short — if it returns nothing, the record is missing from Cloudflare DNS (or you only added the public hostname to the tunnel but not the DNS record, and the tunnel didn't create one automatically). Also check: if using Cloudflare Tunnel via the dashboard GUI, the public hostname does auto-create the DNS record. If using the CLI, run cloudflared tunnel route dns tunnel-name subdomain.osztromok.com manually.sudo apache2ctl -S and check whether the subdomain vhost appears in the list. If not, it's not enabled — run sudo a2ensite subdomain.conf and reload. If it is listed but still wrong, check ServerName in the config matches exactly what's in the Host header (including capitalisation, which Apache treats case-insensitively but typos matter).ss -tlnp | grep 8080 — if nothing's listening on that port, start the app. (2) Is it listening on 127.0.0.1 or 0.0.0.0? The app must accept connections from localhost. (3) Check the backend app's own logs for errors.a-wildcard.conf before blog.conf). Fix: rename the wildcard config to something that sorts last, like z-wildcard.conf, then run a2dissite old-name.conf && a2ensite z-wildcard.conf and reload.Quick Reference — Chapter 6
| Task | Action |
|---|---|
| New static subdomain | Create dir → write vhost → a2ensite → configtest → reload → test locally → add DNS → test externally |
| Redirect www → apex | Vhost with Redirect permanent / https://apex.com/ OR Cloudflare Redirect Rule |
| Wildcard DNS | CNAME record with Name: * → Target: osztromok.com in Cloudflare, proxied |
| Wildcard Apache | ServerAlias *.osztromok.com in vhost, filename should sort last alphabetically |
| Reverse proxy | a2enmod proxy proxy_http → restart → vhost with ProxyPreserveHost + ProxyPass + ProxyPassReverse |
| Test vhost locally | curl -H "Host: sub.osztromok.com" http://localhost |
| Verify DNS live | dig sub.osztromok.com +short → should return Cloudflare IP |
| Check vhost order | sudo apache2ctl -S → lists all vhosts in match priority order |
| Symptom | Likely cause |
|---|---|
| NXDOMAIN / "can't be reached" | DNS record missing in Cloudflare, or not propagated yet |
| Wrong site content served | Apache vhost not enabled, or ServerName mismatch |
| SSL certificate error | DNS not proxied (grey cloud) — switch to orange cloud in Cloudflare |
| 502 Bad Gateway | Backend app not running on the port specified in ProxyPass |
| Wildcard catches specific subdomain | Wildcard vhost sorting before specific one — rename to z- prefix |