The page was fine five minutes ago. I uploaded an edited paragraph, refreshed the browser, and got 403 Forbidden. The virtual host was up. TLS still looked healthy. The HTML file was sitting right where it belonged on the disk. Nothing in the application had “crashed,” because there is no application — just files and Apache.

That outage lasted about as long as it takes to run one find command and notice that a freshly rsynced article had arrived from my Mac Studio as mode 600: readable by me, invisible to the web server user. No drama in the error log. Nobody broke in. The site went dark because I own every step from the editor on my desk to the public URL — and that includes the boring mistakes that only I can make.

This is how a house in Sarasota ends up hosting a small fleet of public websites — consulting, family, ham radio clubs, Minecraft portals and map sites, the odd campaign landing page — and why I still prefer that arrangement to renting someone else’s control panel.

What Lives on the Edge of the Network

One host does almost all of the public web work: proxy.dmz.local, an Ubuntu server in the DMZ with Apache in front and ModSecurity loaded with the OWASP Core Rule Set. Public names resolve to it; private LAN names resolve inside the house. Certificates for the public sites come from Let’s Encrypt. An internal operations console on the same box is locked down to private networks and uses a self-signed cert, because that dashboard is not for the world.

proxy.dmz.local Operations Console showing Apache system metrics, GeoLite2 status, and links to WAF, SSL, and Web Sites dashboards
The homebuilt web server operations console, refreshed every few minutes from a collector script on the server.

The console is how I get a glance at all of the pieces that make my web server work without diving into piles of analytics and logs. Across the top it shows Apache and system health: uptime and version, requests per second, total requests since the last restart, worker and connection counts, load averages, disk and memory. Below that, MaxMind GeoLite2 city and ASN databases used for local IP geolocation on blocked and interesting traffic — no external whois calls, just files that age out and get rebuilt on a schedule. Three more links cover the rest: a WAF Dashboard for ModSecurity/CRS events, Fail2Ban jails, and today’s top blocked IPs — just because I like to keep my eye on the bad guys; SSL Certificates for Let’s Encrypt expiry across every web site domain name on the host; and Web Sites for rolling 24-hour visitors, pages served, and top pages per domain. The green hostname and refresh timestamps tell me the collector ran; if they go stale, something on the box stopped talking.

The sites themselves are mostly static document roots under /var/www/. A few hostnames reverse-proxy deeper into the network — the AwesomeHeroSteve Minecraft map subdomains, for example — but the public HTML for those web sites still starts here.

It is not a huge fleet. It is still enough that “I’ll just FTP one file” stopped being a strategy years ago. Shared navigation fragments, a sitemap tool, daily traffic reports, and a WAF that actually blocks things keep me from treating a dozen public domains like a hobby homepage.

Self-hosting does not remove outages. It removes the fiction that they were caused by someone else.

Apache, On Purpose

When people find out I run this stack, the first word that usually throws them is Apache. The fashionable default for new projects is often nginx, and for reverse-proxy-only jobs it is excellent. I already know Apache deeply — and I should. I’ve been working with it for over 30 years. The sites need virtual hosts, TLS termination, access control by IP for internal tools, reverse proxies for a few backends, and Server Side Includes so headers and footers are not copy-pasted twelve times.

SSI shows up in two dialects here. Some sites enable includes for every .html file. Others use Apache’s XBitHack: only pages with the execute bit set get parsed. That second one is useful and easy to forget. Deploy a clean rsync and forget to chmod +x the public pages, and the navigation includes vanish and the page looks half-built — not because the HTML broke, but because Apache stopped treating the file as something to process.

None of this needs a container platform. It needs configuration files in version control, a habit of apache2ctl configtest before reloads, and remembering that “it works in my laptop preview” is not the same as “www-data can read it on the proxy.”

The Firewall in Front of the Files

Static HTML is a smaller attack surface than a content management system, but it is not invincible. Bots still hammer login paths that do not exist. Scanners still probe for wp-admin, PHP shells, and yesterday’s CVE of the week. So the proxy runs ModSecurity with the OWASP Core Rule Set, kept closer to upstream than the lagging distro package alone would allow.

A small daily job checks GitHub for newer CRS releases, installs them into the path Apache actually loads, holds the apt package so a routine upgrade cannot silently downgrade the rules, and mails me when something changes or fails. That is not glamorous work. It is the same habit I want at the edge of a client network: know what version is live, when it last changed, and whether the update job still works.

Blocking one request is not the same as getting rid of the source. That is where Fail2Ban comes in. It watches the Apache and ModSecurity logs and, after enough bad behavior, adds a temporary ban in iptables so the offender stops reaching the sites entirely. On this host the jails are deliberately boring and specific: repeated ModSecurity hits, scanners hunting for .env / .git / WordPress config files, xmlrpc.php hammering that has no legitimate business on a static fleet, plus the usual SSH brute-force jail. ModSecurity decides the request is hostile. Fail2Ban decides the IP has worn out its welcome.

I care about more than “Apache is listening on 443.” The Zabbix templates for this host watch blocked attacks, Fail2Ban bans, and — most importantly — whether the rule engine is still actually enforcing. A WAF that slipped into detection-only without anyone noticing is worse than no WAF, because it teaches you to trust a green light that is lying. The ops console treats ModSecurity and Fail2Ban together for the same reason: a blocked request and a banned IP are part of the same problem.

Every morning, a traffic report digests yesterday’s vhost logs: hits, status codes, and ModSecurity denials broken out per site. Most days it is quiet noise. Some days it is a reminder that bots keep working overnight while the rest of the house is asleep.

Deploy Is Half the Job

Content lives in a git-backed tree on a Mac, synced through iCloud between the Mac Studio in my office and the MacBook Air I work from outside the office, then pushed to the proxy with rsync: stage under /tmp, promote into /var/www/<site> with root, fix permissions, spot-check with curl. There is no CI pipeline that ships on a green checkmark. There is me — someone who can break production with a mistyped path, and who can fix it without opening a ticket with a cloud provider.

The permission trap gets its own paragraph because it is so ordinary. Files that leave a Mac via rsync often arrive as owner-readable only. Apache runs as www-data. Directories need to be traversable; files need to be world-readable (or at least group-readable by the server). Skip that step and you get 403s that look like security events and feel like mystery outages. The fix is not clever. It is a checklist I run every time:

Looks Done

What Seems Finished

  • rsync finished without errors
  • The file exists on the server
  • I can open it over SSH as myself
  • The browser cache is probably the problem

Actually Done

What Actually Counts

  • Directories 755, files 644 (unless XBitHack needs +x)
  • Apache can traverse every parent path
  • SSI partials and public pages still parse
  • curl -I returns 200, not 403

Sitemaps are another deliberate choice: they are regenerated by a script on the proxy when pages are added or removed, not by a timer that quietly rewrites priorities while I am not looking. I have been burned by automation that “helped” by treating every HTML file as equal and by treating directory prefixes as ignore rules when only exact paths should have been ignored. Doing it by hand after a content change is slower. It is also honest about what changed.

Why Keep the Blast Radius at Home

Self-hosting public sites from a business fiber internet connection at home is not the default advice in 2026. Object storage in the cloud and a content delivery network are fine tools. Managed WordPress sites are fine for people who do not want to deal with all of this. I keep the web sites here for reasons that are part professional and part personal.

Professionally, it is practice. Virtual hosts, WAF tuning, certificate renewal, log hygiene, reverse proxies, careful deploys — the same skills I use on client work, practiced on systems I am allowed to break and fix without a change board. When ModSecurity false-positives a form, I learn something about rule exclusions. When a reverse proxy to a Minecraft map hiccups, I learn something about backends and timeouts.

Personally, the content is not generic. A family article, a club bylaws PDF, a reflector how-to, a photography placeholder that might grow into a portfolio again — these are small sites with real names attached. I do not want their availability, their headers, or their log retention decided by a plan tier I forgot I was on. If something is wrong, the path from symptom to root cause stays inside a network I already monitor for cooling, disks, and failover.

There is also a simple preference: open source where it matters. Apache, ModSecurity, CRS, Fail2Ban, Ubuntu, Let’s Encrypt, Zabbix on the side. The stack is boring on purpose. Boring is a good thing when the alternative is a CMS plugin ecosystem that rots every six months.

You can lock every door for safety. That only works if the right person still has a key when something goes wrong.

What This Is Not

I am not arguing that every household should host web sites for a consulting firm in the garage, or that Apache beats nginx in some universal ranking. Static HTML still needs care — certificates expire, rulesets update, disks fill, hurricanes visit Florida, and people rsync with the wrong flags.

It is just a description of what works here: a DMZ proxy that answers for a dozen public names, a WAF I actually watch, deploys that are not done until permissions and HTTP status say they are done, and an operator who knows that “the website is down” might mean anything from a CRS block to a file mode bit set by a laptop two rooms away.

The Bottom Line

Running public websites from home is not about proving I can escape the cloud. It is about owning the path from keystroke to response headers. Apache, ModSecurity, and Fail2Ban do the unglamorous work at the edge of the network. Static web site files keep most things quiet. The real discipline is the checklist after every deploy: can www-data read what I just published, and does the open internet agree? When both answers are yes, the house is doing its job — serving pages, logging what matters, and failing in ways I can still explain.