I run more than two dozen sites and apps on a single server with four CPU cores and about 7 GB of RAM. Nothing on it is fashionable. That is the whole strategy.
Most of what I build lives on one Ubuntu machine: marketing sites, a few client projects, small internal tools, a blog, a fiber-mapping app for ViberNet, and the sites for OpenLoop. People are sometimes surprised when I tell them how modest the hardware is. The surprise usually comes from assuming that "serious" means containers, orchestration, and a cloud bill with a dozen line items.
It doesn't have to. Here is what I actually run, why, and where the approach starts to strain.
The stack is deliberately boring
The web server is nginx. PHP sites run through a single PHP-FPM pool. Most databases are MariaDB, listening on localhost only, so nothing outside the machine can reach it. Small tools that do not need a database server use SQLite, which is just a file. Exactly one app uses PostgreSQL with PostGIS, because a fiber map needs real geographic queries, and that is the only reason it got in.
TLS comes from Let's Encrypt through certbot, renewed automatically on a timer. There is no load balancer, no container runtime, and no message queue. Every piece is something I can debug at 11 p.m. with a terminal and a log file.
The rule I follow is simple: a new technology has to solve a problem I actually have, not one I might have in two years. Most of the time it does not clear that bar.
This summer I removed Node from two production sites
Two of my sites started life as Next.js apps. The marketing site for Viral Pulse Media now builds to plain static HTML with a small PHP script, so there is no Node process and no framework at request time. The ISP website for ViberNet is now vanilla PHP with Tailwind, and the pm2 process that used to keep it alive is gone.
The reasons were practical. A static page cannot crash under load, cannot leak a memory problem into the next request, and cannot go down because a dependency changed. For Viral Pulse the hard requirement is a 100 out of 100 PageSpeed score, and it is far easier to hit that when the browser is handed finished HTML and a small stylesheet than when it has to hydrate a framework first.
I still use frameworks where they earn their place. The fiber-mapping app is Laravel, because it has logins, roles, and a real data model. The point is not "no frameworks." It is that a brochure site should not carry the weight of an application.
Enjoying this?
Get Last Mile in your inbox, daily-ish writing on ISP economics, AI in production, and building in Kashmir.
What performance work actually looked like
The clearest example was a coaching-centre website I built. On mobile, its Largest Contentful Paint measured 9.1 seconds in Lighthouse, which is the time until the main content shows up. After the fixes it measured 3.0 seconds.
There was no clever trick. The page was pulling stock photos from another company's server, and a few local images were far bigger than their display size. One director's photo was 1766 by 1752 pixels, shown at a fraction of that. I downloaded the hot-linked images, resized everything to the size it was actually displayed at, converted it to WebP, and served it from the same machine. That director photo dropped to 42 KB.
I did the image work with a Node library called sharp, installed in a scratch folder, since the server has no ImageMagick. It is one of the few places where I reach for Node, and it never runs in production.
Two mistakes that taught me more than any tutorial
The certificate that pointed at the wrong site. When I added a new subdomain for ViberNet, certbot matched the wrong server block. The main ISP site uses a wildcard server_name, and certbot decided that was the block to update. It rewrote the certificate lines on the live site to point at the new subdomain's certificate. Nothing errored. The main site would simply have shown certificate warnings to visitors.
I caught it because I read certbot's output line by line instead of assuming success. One line said it had deployed a certificate to the main site's config, which is not what I had asked for. I checked each hostname separately with openssl s_client, restored the right certificate reference, and gave the new subdomain its own self-contained server block so it no longer depends on the wildcard one. The lesson: certbot reports what it did, so read it, and check every hostname after any certificate change.
The DNS answer that would not update. After I pointed a domain at the server, the machine's own resolver kept returning the old answer for hours, long after public DNS had moved on. That is a nasty problem, because every check I ran from the server said "not working" while the site was fine everywhere else. Even a Lighthouse run hit a 404 from it. The fix is one command, resolvectl flush-caches, but only if you know the server is lying to you about its own DNS.
The habit that matters most: writing it down
The hardest part of running many small sites is not the software. It is remembering what is where. I keep a plain-markdown notebook on the server itself, one note per site with its stack, docroot, database, and quirks, plus a log of every change I make. There is no wiki software and no build step. It is text files, and that is why it keeps getting updated.
When something breaks, that notebook is the first place I look, and it has paid for itself many times over. A server you cannot explain from memory has to be explained from documentation, and documentation only survives if updating it is easy.
Where this approach stops working
One machine is one failure domain. If it goes down, everything on it goes down together. A single PHP-FPM pool also means one badly behaved site can use up workers that the other sites need. I know both of these are the price of the setup, and they are the reason I watch resource use instead of assuming there is always headroom.
I would split things up the moment one site's traffic or risk justified its own machine, or a client needed guarantees I cannot make from a shared box. I have not reached that point, so I have not paid the complexity cost of preparing for it early.
If you take one thing from this, let it be that boring is a feature. At my scale, a stack I fully understand beats one that looks impressive, and the number of things that can go wrong stays small enough to hold in my head.