Build log: running production SaaS on k3s behind Cloudflare Tunnel
Our client portal, marketing site, and APIs run on hardware we own — with zero inbound ports open. Here is the architecture and what it costs us in practice.
Everything EZ APPS ships — the marketing site, the client portal, the billing API, this blog — runs on a k3s cluster on hardware we own. There is no cloud load balancer in front of it, and more importantly: no inbound port is open to the internet at all.
This is a build log of that setup, because it is the architecture we quietly recommend to clients who want real hosting without a five-figure cloud bill.
The shape of it
Three moving parts:
- k3s — lightweight Kubernetes. One binary, systemd service, done. It schedules our containers, restarts them when they die, and rolls deployments with zero downtime.
- Cloudflare Tunnel — a small daemon (
cloudflared) inside the cluster makes an outbound connection to Cloudflare’s edge. Traffic for our domains rides back down that tunnel. The router forwards nothing. Port scans find nothing. - GHCR images — every service builds in CI into a versioned container image (
v20260818...). Deploys are one line: point the deployment at the new tag. Rollbacks are the same line with the old tag.
The tunnel’s ingress rules do the routing that a reverse proxy would normally do — hostname and path matching, straight to cluster services:
- hostname: ezappsllc.com
path: ^/app(/.*)?$
service: http://portal:8080
- hostname: api.ezappsllc.com
service: http://backend:5000
- hostname: blog.ezappsllc.com
service: http://marketing:8080
What this buys us
Security posture, mostly for free. No open ports means the whole class of “scan, find, exploit” attacks never reaches us. TLS terminates at Cloudflare’s edge; certificates are their problem.
Immutable deploys. Containers are rebuilt, never edited. If a change is not in git, it does not exist in production. That discipline has saved us more than once — there is no “who edited this file on the server” archaeology.
Real rollbacks. Because every deploy is a version tag, rolling back is not a restore-from-backup event. It is a thirty-second command.
What it costs
Honesty section. This setup is not free:
- You own uptime. If the power or ISP goes, so does the site. Cloudflare keeps the edge up, but the origin is yours. We accept that trade for the workloads we run.
- Kubernetes has a learning curve, even the k3s flavor. The first week is configmaps and rollout commands. It levels out.
- Stateful services need care. The database wants backups you actually test. A managed database is still a reasonable line item even in a self-hosted stack.
The verdict
For a small software company that ships several products, this stack has been the right call: cloud-grade deploy discipline, hardware economics, and a security posture that beats most exposed VPS setups by default.
If you want the same architecture for your own product, that is literally a thing we build for clients — it is the Hosting & DevOps bucket on the main site.