Your Web App Is Too Professional to Self-Host
Production-grade architecture becomes a support-ticket generator the moment strangers run your code on their own boxes.
There's a genre of blog post that walks you through an architecture decision and ends in triumph. kopper's essay, which hit the Lobsters front page this week, is the other kind: a slow-motion catalog of every optimization a self-hostable web app can't have. Offload static files to the reverse proxy? Someone's proxy is a Kubernetes-only ingress that can't see your container's filesystem. Add cache headers? One admin's Caddy plugin mangles responses, another forgot nginx's proxy_cache_lock, a third is double-caching through Cloudflare. Fix Japanese search with a Postgres extension? You just asked every admin to recompile the shared Postgres instance that four other apps depend on.
The essay ends where much of the ecosystem has already arrived: give up, bundle a reverse proxy, a tuned cache, Postgres with extensions, the frontend, and the backend into one fat Docker image, and tell admins to point their existing proxy at it. Deployments not using Docker: no longer supported.
The diagnosis is exactly right. The conclusion deserves pushback — because it treats the production architecture as fixed and the deployment as the variable, and the projects actually winning at self-hosting do the opposite.
The contract you can't write
Every pain in that catalog is the same pain: an assumption about a machine you will never see. Production teams get to write an SLA with their own infrastructure — this cache, tuned this way, in front of that app server, backed by a dedicated Postgres where installing an extension is a ticket, not a negotiation. Self-hostable software gets no such contract. The reverse proxy is whatever the admin already runs. The database is shared to amortize its overhead. The cache is whatever middleware shipped with your framework, because you can't require Vinyl Cache — the project formerly known as Varnish, renamed in March after its trademark split with Varnish Software — on somebody else's box.
The details compound. No-Vary-Search, the header that would let you declare which query parameters actually matter for caching, is still a Chromium-only experiment. Postgres's built-in full-text search can't segment text in scripts that don't use spaces, so the moment a Japanese user shows up, you're evaluating PGroonga — a genuinely good extension that solves the problem in place, and also the moment your "easy to deploy" promise dies, because now the shared Postgres needs third-party code compiled into it.
None of these is a bug. It's a config matrix, and the support burden of a config matrix grows multiplicatively while your efficiency gains grow linearly. The fat container is a rational response: it collapses the matrix back to a single supported cell.
We've been here before
It's also not a new response. The most successful self-hosting substrate ever built was PHP on shared hosting, and its genius was a brutally narrow contract: you upload files; the host owns Apache, MySQL, and the PHP version. WordPress rode that contract to roughly 40 percent of the web. Sandstorm spent the mid-2010s trying to formalize a richer substrate — sandboxed apps, standardized grain storage — and folded as a company in 2017. YunoHost and Cloudron are the current attempts, and kopper's essay nods at the "one-click deployment" ecosystem with the right mix of respect and despair.
The Docker mega-container is FTP-upload reinvented: one port, and everything behind it is mine. The history says narrow contracts win. But the container version has a cost the PHP version didn't — it bundles production architecture instead of shedding it. The essay's own closing tally makes the case: a single page load parsed and rewritten by the admin's proxy, then the bundled proxy, then the bundled cache, then the app, with the frontend making internal API calls of its own. Five HTTP hops on hardware chosen for its electricity bill.
The stack was the variable all along
Put Mastodon next to GoToSocial. Same protocol, same job. Mastodon wants Rails, Postgres, Redis, Sidekiq workers, and a Node streaming server; GoToSocial is one Go binary with SQLite, happy on a Raspberry Pi. Forgejo, PocketBase, Vaultwarden — the pattern repeats across categories, and it's not an accident. Each one deleted a wall of kopper's maze by construction. Embedded static assets mean there's nothing to poke through container boundaries. Server-rendered HTML means the "every competent SSR framework is JavaScript" trap never springs, because there's no second runtime to deploy. SQLite means the shared-Postgres-extension problem evaporates along with the shared Postgres.
To be fair to the essay, one wall survives: multilingual search. SQLite's FTS5 has the same blind spot for unwritten word boundaries that Postgres does, and its trigram tokenizer is a partial answer at best. CJK search on hobby hardware is legitimately hard everywhere. That point stands.
But the general lesson inverts the essay's fatalism. If you're building software you intend other people to run, the deploy contract is the product, and you buy it with architecture, not documentation:
- One process, one port. If the install instructions are longer than "run it, point your reverse proxy at it," you lose the audience the essay describes — accurately — as unwilling to read a novel.
- SQLite in WAL mode by default, Postgres as an option, Postgres extensions never. Every extension you require halves the number of admins who'll deploy you.
- Serve your own static files with immutable cache headers, cache in-process, and treat any external cache as a bonus you don't depend on.
- Ship the container as packaging, not as architecture. A container wrapping one binary is a convenience; a container wrapping six services is a confession.
Who's actually eating those cycles
The essay's kicker lands hardest: after all that engineering, the request crawling through five layers turns out to be an AI scraper on a residential proxy, fetching a page revision that stopped mattering two years ago. That's not a rhetorical flourish — it matches LWN's reporting on the scraper wave: crawlers riding residential proxy networks, ignoring robots.txt, hitting one admin's personal Forgejo 600,000 times a day until Anubis, the proof-of-work challenge now guarding GNOME and FFmpeg infrastructure, cut it to about a hundred.
At hobby scale, capacity planning is no longer about your users. It's about other people's crawlers. Which is the final argument for the small stack: a single binary behind Anubis will outlive a perfectly tuned cache hierarchy you can't make anyone deploy.
The web server deployment model isn't what breaks at hobby scale. What breaks is professional architecture transplanted into machines you don't control. Narrow the contract and most of the essay's pain — beautifully cataloged, genuinely felt — simply never happens.
Sources & further reading
- the web server deployment model breaks at hobby scale — w.on-t.work
- Lobsters discussion of the essay — lobste.rs
- On Vinyl Cache and Varnish Cache — vinyl-cache.org
- Anubis sends AI scraperbots to a well-deserved fate — lwn.net
- PGroonga: fast full text search for all languages in PostgreSQL — pgroonga.github.io
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 1
self-hosting is just outsourcing support to your users. hard pass unless you're deliberately targeting that crowd.