Everything powering awrealty.homes runs on a single N95 mini PC sitting in my house. Four cores, 7.5GB of RAM, no cloud provider, no managed services, no monthly infrastructure bill. Nothing runs as root, and the account that deploys the site is not the account that runs it. Here's how it all fits together.
The Host: Proxmox & One Container
The N95 runs Proxmox, and the whole site lives in a single unprivileged LXC container: Debian 13, 3GB of RAM, all four cores available to it. Inside that container are Nginx, the Cloudflare Tunnel client, and the application itself—one Rust binary that serves every page.
It used to be three containers. A 256MB Alpine box held the static frontend, a 1GB Alpine box ran the backend service, and a third ran self-hosted monitoring. Splitting them up felt like the disciplined choice at the time, and it was mostly ceremony: three sets of updates, three firewall postures, and a network hop between two halves of the same application. When the site moved to server-side rendering, the frontend and backend stopped being separate things, and keeping them apart stopped making sense.
So I consolidated. The interesting part is the number: one container running the entire site uses less host memory than the three it replaced. The old ones are stopped rather than deleted—a stopped container is a one-command rollback, a destroyed one is a rebuild, and disk is cheaper than certainty.
The Application: One Rust Binary
The site is a Dioxus app written entirely in Rust, and it ships as a single binary. That binary does both jobs: it renders every page to finished HTML on the server, and it serves the compiled WebAssembly bundle that takes over in the browser afterward for the interactive pieces—the theme toggle, the nav, the carousels. A visitor gets complete markup on the first response and never waits on JavaScript to see content. A crawler gets the same thing and never has to run any.
The dependency list is short on purpose: Dioxus, Axum, Tokio, Serde, Tower-HTTP, and Tracing. That's the whole tree. Axum and friends are feature-gated behind the server build, so the WebAssembly bundle never compiles them and never carries their weight to the browser.
There's no database yet, and that's deliberate rather than unfinished. Nothing on the site currently needs to remember anything between requests, and a database I don't need is a backup I have to take, a migration path I have to maintain, and one more thing that can be down at 2 AM. When the contact form lands it'll be SQLite through sqlx—queries checked at compile time, and the entire database is one file, which makes backing it up completely uninteresting. That's the highest compliment I can pay a backup strategy.
The Network: Cloudflare Tunnel, Nginx & Tailscale
The networking layer is designed so that nothing is exposed to the public internet directly. When someone visits awrealty.homes, the request flows through a Cloudflare Tunnel—a secure outbound-only bridge that lets Cloudflare talk to the N95 without ever opening a port on my home router or revealing my home IP. Cloudflare terminates TLS at the edge, so Nginx inside the container listens on plain HTTP and acts as the traffic cop: hashed assets come straight off disk with a one-year cache, and everything else goes to the Rust binary for rendering.
There's no API subdomain anymore. When the frontend and backend are the same program, server functions register their own routes on the main app, same origin—so the extra hostname was just a DNS record with a story attached. Deleting it removed a moving part and a CORS policy in one go.
For management, I use Tailscale to create a private, encrypted network exclusively for my devices. This lets me SSH into the N95, deploy code, and manage containers from my laptop without exposing SSH to the world. The result is zero inbound ports open to the internet. Everything either goes through the Cloudflare Tunnel (public traffic) or Tailscale (my traffic).
The Pipeline: Ship On Push, Roll Back On Failure
One GitHub Actions workflow handles the whole deploy. Push to main and it builds, connects to the box over Tailscale, rsyncs the release up, and restarts the service. I never touch the server by hand.
Each build lands in its own directory named after the commit, and going live is a symlink swap—a single rename syscall, so there's no instant where the live path points at nothing. Releases are never modified in place, only pointed at, which is what makes rolling back immediate instead of a re-copy. The previous good release keeps its own symlink, so undoing a bad deploy by hand is one command even if GitHub and my network are both unavailable.
Then it checks its own work, and this is the part I care about most. After the restart, CI polls the running app and verifies real behavior through Nginx: the homepage renders, robots.txt is served, the sitemap comes back non-empty, and an unknown URL actually returns a 404. If any of that fails, the pipeline points the live symlink back at the last good release, restarts, and polls again to confirm the old version is genuinely answering. Then it still reports the build as failed, because nothing can un-fail the step that already failed. A red build and a live site is the correct outcome.
I built it that way because I watched the alternative happen. A sibling project's deploy script printed "Deployment Successful" unconditionally—it copied the binary under the wrong filename and restarted a service that kept running the old one. It shipped nothing for three months and nobody noticed, because every single deploy was green. A restart command exiting zero tells you a process started. It tells you nothing about whether your site works. So now the pipeline has to prove it.
Resilience & Backups
Keeping the site alive matters more than I'd like to admit. The N95 sits on an uninterruptible power supply, so if the power drops there's a buffer before anything shuts down, and it shuts down gracefully rather than dying mid-write. The BIOS is set to auto-boot when power returns, Proxmox brings the container back up on its own, and the site comes back without me. The auto-boot was the part that fought me. It lives in the BIOS under "State After G3"—G3 being the ACPI term for "mechanical off," which is why searching the menu for anything about power loss turns up nothing. Its value is a bare integer with no legend explaining what any of the numbers do. Set it wrong and the failure is completely silent: the machine just stays off after an outage, and nothing logs it, because nothing is running to log it. I know it works because I've cut the power and watched the whole thing come back, more times than I can count. Untested recovery isn't recovery.
Monitoring is external only, and that's a correction rather than a shortcut. I used to run Uptime Kuma on the same host it was watching, which sounds fine until you think about the failure that actually matters: if the house loses internet, the monitor loses internet too, and the thing that was supposed to tell me the site is unreachable is now equally unreachable. A monitor that can't report the outage it's most likely to encounter isn't monitoring. UptimeRobot checks the live site from outside my network instead, and it looks for the words "AW Realty" in the response rather than just a 200—because this codebase has already produced pages that returned a perfectly healthy status code while being visibly broken.
Backups run in two stages. Proxmox snapshots the container nightly and keeps a week locally, then a script ships those snapshots to cloud storage at 5 AM with a 30-day retention window. Writing locally first and copying second is the important detail: pointing the snapshot job straight at a network mount means a stalled connection can interrupt it mid-write and leave the container locked, which is exactly the state I once found every container on this host in.
But the real lesson wasn't about backup software. One job died mid-run and left a lock file behind, and every night afterward the job failed on the lock rather than on the original problem. It ran that way for six weeks before I caught it. The trigger was a genuine fluke, the kind of thing I may never see again—and that's exactly why it went unnoticed for so long. Routine failures get spotted quickly. Anomalies are the ones that sit there quietly, because nothing about the system looks wrong until you go looking. So the fix wasn't a better backup tool, it was making failure loud: both alerting paths now mail me, and both were verified by deliberately breaking them. An untested alert is just a comment in a config file. A backup you haven't proven you'd hear about failing isn't a backup, it's a hope.
What I'd Change
If I'm being honest, frontend work isn't my strong suit. I'm a backend guy. I like seeing data move, watching systems respond, getting the result I want. Design doesn't come as naturally, and I've had to work at it here in a way I haven't had to work at the infrastructure.
This site has gone through several iterations, and the current one won't be the last. That used to bother me. It doesn't anymore—every rewrite on this list happened because I'd learned something specific that the previous version couldn't take advantage of, and the versions have gotten measurably better each time: fewer moving parts, less memory, faster pages. Not being done is what it looks like when you're still paying attention.