Why we run client apps on a multihost pattern
One bad deploy taking down two production apps is a failure of architecture, not a failure of the developer. Last December a schema migration to our invoicing app cascaded into Kitchen POS next door — thirty minutes of downtime, a handful of refund requests, and a very late night fixing tables we should not have touched. The multihost pattern is what we formalized so that never happens again.
What We Mean by "Multihost"
Let's define terms so we're on the same page. When we say "multihost," we mean one Linux host — or a small, focused cluster — running many independent virtual hosts. Each vhost gets its own DNS name, its own web root, its own data directory, and its own Git repository synced on a predictable cadence. They're neighbors on the same machine, but they don't share application databases, they don't share cron environments, and they definitely don't share blast radius when something goes sideways.
An example makes this concrete. Our invoicing SKU lives at something like `invoicing.decisionsciencecorp.com`, with its data living in `/var/www/
This isn't WordPress multisite, where one WordPress installation serves dozens of unrelated businesses. It's not a monorepo serving every client from the same codebase. It's closer to what a competent sysadmin would have done a decade ago — clean boundaries, predictable paths, boring permissions — but formalized enough that we can hand it to a new engineer and they'll understand the layout in under ten minutes.
Why Not One Giant VPS for Everything?
The first reason is blast radius. When everything lives on one server, one bad deployment takes down everything. We don't mean this in a fear-mongering way — we've simply seen it happen enough times to respect it. A schema migration meant for the invoicing system grabs a table that the POS system is also touching. A permissions fix in one client's web root accidentally breaks another client's data directory. These are solvable problems, but they're also avoidable problems, and we'd rather avoid them.
The second reason is ownership boundaries. Each vhost has a clear owner — a product, a team, or a client. When someone new joins and asks "who owns the invoicing system?", the answer is straightforward: it's the vhost in `/var/www/
The third reason is billing clarity. When a client's hosted service pauses or terminates, we retire their vhost — remove the directory, revoke their cron jobs, let the DNS expire. On a shared server, retiring a client means archaeological work: digging through cron logs to find their jobs, untangling their data from someone else's backups, and hoping you didn't miss a symlink. Multihost makes "remove this client" a three-step process instead of an afternoon.
The fourth reason is onboarding cadence. Our client development workflows depend on branch-based staging. A developer can spin up `dev.invoicing.decisionsciencecorp.com` pointing to the `dev` branch while production tracks `main`. That staging environment runs on the same multihost, same cron sync cadence, same permissions model — just a different Git branch and a different `BRANCH` environment variable. If that experiment fails, we delete the `dev` vhost and nothing in production even flinches.
We'll acknowledge the exception here: for a single product, one small team, an early MVP where everyone in the room knows every line of code — a single VPS is fine. We're not dogmatic about this. The multihost pattern kicks in when the number of independent properties exceeds what you'd want to debug at 3 a.m.
The Boring Ops That Make It Work
The pattern only works if the operational basics are boring and consistent. Here's what that looks like in practice.
Predictable deploys. Each vhost has its own GitHub repository. A cron job runs every two-ish minutes, pulling the latest from the vhost's default branch and replacing the web root. There's no magical CI pipeline — just a scheduled sync and a brief restart of the PHP-FPM pool. That's it. When a developer pushes to the invoicing repo, their changes are live within a few minutes without anyone logging into a server.
TLS. We use Let's Encrypt for certificate management, one certificate per hostname. There's a brief wait when provisioning new vhosts — certificate propagation isn't instant — but the automation handles renewal without manual intervention. We're not managing wildcard certificates across unrelated clients; each hostname gets its own certificate, and they renew independently.
SQLite on multihost. Here's where the permissions matter. The database file lives in a `db/` directory sibling to `html/`, owned by the same `www-data` user that nginx reads. It's not a shared MySQL instance where one client's query hog impacts everyone. It's a small, boring file with straightforward permissions that we copy to a backup location before any schema migration. We've written more about this in our deploy scripts, but the core idea is: boring permissions prevent 3 a.m. pages.
DNS as a separate concern. Our DNS and TLS provisioning lives in one lane (Ada handles this), while application code lives in another lane (engineering handles this). That separation means DNS changes don't require application access and vice versa. It's not a philosophical split — it's practical. When TLS fails, we know exactly which lane to debug.
Real Shapes We've Used
We've run a few different shapes under this pattern, all without using client names.
Hosted product subdomains. Our internal project management tool runs as a hosted SKU — a product we built and host for ourselves. It's a multihost vhost like any other, with its own repo, its own cron sync, its own database. That's been stable for over two years.
Open-source repo plus hosted SKU. Our invoicing system is the best example. The core lives in a public GitHub repository, but our hosted instance runs against a private vhost with a private database. Clients interacting with invoicing.decisionsciencecorp.com never see the internal repo — they just see a working application.
Branch-based dev staging. When we're actively developing a product, its `dev.*` subdomain tracks the `dev` branch while production tracks `main`. That's been invaluable for testing migrations and new features without risking production data. When `dev` looks solid, we merge to `main`, and the production vhost pulls the new code on its next sync.
Client dev subdomains. For clients experimenting with Shopify integrations or DTC campaigns, we provision a short-lived vhost, set up the DNS, and let their repo sync. When the experiment ends, we tear down the vhost and let DNS expire. No archaeologists needed.
What This Is Not
This isn't Kubernetes cosplay. We're a three-person engineering team — we're not running Helm charts or managing pod orchestrations. The multihost pattern works because it's simple: one server, many directories, predictable cron jobs, boring permissions. If you need horizontal scaling beyond a single host's capacity, this pattern isn't your answer.
This also isn't unlimited horizontal scale. There are hard limits — CPU, memory, disk I/O on a single machine. We're honest about that. When we hit those limits, we migrate to the next host and load-balance. But for the workloads we manage — small-to-medium web applications, mostly SQLite-backed — a single host running a dozen vhosts works fine.
Finally, this isn't vendor lock-in. The pattern is Linux and nginx; it runs on any VPS provider. We're not dependent on AWS, GCP, or any specific cloud. If the Dallas host stops meeting our needs, we spin up another host and replicate the pattern.
Close + CTA
If you are running several small web apps and tired of permission leaks across shared hosts, tell us what you are hosting and what broke last. We like predictable deploys and boring infrastructure that stays boring.