I run this website off a Raspberry Pi in my LAN. The whole website lives in a Docker container, uses a Cloudflare Tunnel, and stays reachable to the world, but only the parts I want reachable. Why? Here’s the long version.
Short version: I like owning my stuff and wanted to reuse stuff I had rather than pay for something new. No SaaS upsells, no sudden “pricing updates,” no vendor lock-in. Just containers and config files I understand.
Small caveat, this post isn’t a full walkthrough. Just a nudge in the right direction. If you’ve got a Raspberry Pi, a weekend, and some curiosity, you’ll figure it out.
The stack
Here’s what powers my setup:
- Ghost runs in a Docker container, using SQLite for the database (though MySQL works fine too).
- A Cloudflare Tunnel container proxies traffic from the internet to the Pi, no ports are exposed directly.
- Portainer manages the stack with a GUI. One click to update, redeploy, or debug.
- The Pi uses an external SSD via USB 3.0. No SD card, no corruption risk.
- The Pi is isolated from the rest of my LAN.
- It’s backed by a UPS to ride out power cuts without killing the filesystem.
- HTTPS certs come from Cloudflare, handled at the edge.
- All secrets and domain configs are stored as environment variables, not inside the Docker Compose YAML.
- The public site lives at
arbitraryentity.info
.
It’s simple, reliable, and rebuildable in minutes if something breaks.
The old Ghost gotcha
Ghost is great, until you realize the /ghost
admin panel is always there. If you self-host it, that login page is open to the entire internet by default, just like /wp-admin
on Wordpress.
You can block bots with a robots.txt
. You can rename /ghost
to something else. But that’s just security by obscurity. I wanted a real lock, which led me to:
Plan A: “Two Access Apps” (the theory)
Cloudflare Access lets you protect parts of your site by identity. So I figured: two “apps.”
- App 1:
/ghost*
→ Allow only me. - App 2:
/*
→ Block everyone else.
Cloudflare matches path rules longest-first. So /ghost*
should stay open only for me, while the catch-all blocks the rest.
The reality: the Access rabbit hole
Turns out Cloudflare’s free plan doesn’t support granular path exceptions in policy logic. If your “Allow” is too broad, the catch-all “Block” doesn’t always win. The result? /
sometimes shows the Cloudflare login screen even when it shouldn’t.
So I ditched it.
Plan B: the real fix
I moved the admin interface to its own subdomain - let's call it example.arbitraryentity.info
.
The Tunnel routes both domains arbitraryentity.info
and example.arbitraryentity.info
to the same Ghost container.
- The public site at
arbitraryentity.info
stays open. - The admin side at
example.arbitraryentity.info/ghost
is protected with Cloudflare Access: email + OTP required to get to the real Ghost login page, both of which only I would know. - A Cloudflare WAF rule blocks all other paths on the admin subdomain, so
example.arbitraryentity.info
with no/ghost
just throws a hard 403. - On the main domain,
/ghost
is also blocked with a WAF rule to prevent accidental leaks.
Result: no visible admin panel to bots or scanners, and only I can get in.
One tiny gotcha: /cdn-cgi/access/
When Cloudflare Access does its login handshake, it hits /cdn-cgi/access/
in the background.
If your WAF rule only allows /ghost*
, that handshake fails after login and you hit a 403.
Fix: Add an exception for /cdn-cgi/access/
in your WAF rule.
Why it works
- No open admin endpoint for bots to sniff
- TLS is handled at the Cloudflare edge
- The main and admin domains are cleanly separated
- No sketchy
robots.txt
tricks or route renaming - It works fine on Cloudflare’s free plan
Bonus: backups next
Next up: daily automated backups. I’ll likely cron a job (or use a lightweight container) to export the Ghost content folder and DB dump to an offsite store.
If the Pi dies or I break the theme, I can rebuild it all in minutes.
Lessons learned
- Cloudflare Tunnel + Zero Trust is underrated for hobby projects
- Use a separate subdomain for anything sensitive
- WAF rules are better than struggling with Access policy hacks on the free plan
/ghost
is the first thing bots poke on any Ghost blog - don’t help them out
And probably the biggest one: