Background
Purcell Analytics runs a portfolio of 14+ public-facing web properties — this site, Quantaize, the Autonomous AJ Command Center, civics_matter, name_bay_bay, the BBQ Atlas series, pyle_on_python, pick_prop_punt, social_sprint, and others — each with its own brand, audience, and content shape. The default way to host 14 sites is to spin up 14 droplets, each with its own SSL, monitoring, deploy automation, and bill. Multiplied out, that's roughly the cost of a small car in annual hosting fees, plus a meaningful fraction of someone's week spent on infra ops.
This case study covers how the entire portfolio runs on a single DigitalOcean droplet and a small set of shared services — and why that's the right tradeoff for a small consulting practice running its own stack.
The problem
The constraints are practical:
- Each site is a real product with its own audience. They can't share a code base, a brand, or a domain. Multi-tenant tooling that hides those differences is the wrong abstraction.
- But each site individually doesn't justify a dedicated $24/month droplet, a dedicated database, or a dedicated CI runner. At 14 sites, those line items add up.
- Operationally, every additional droplet is another thing to keep patched, monitored, and backed up. Running infra on 14 boxes for a one-person team is a way to spend the next decade on infra instead of building.
The version of this problem that actually breaks small consulting practices isn't the dollars. It's the operational drag. The third or fourth time you have to renew a Let's Encrypt cert manually, or restart a service across multiple boxes, or chase a deploy bug that's specific to one droplet, you start cutting back on what you ship.
The approach: one droplet, many tenants, shared services
The portfolio runs on a single 2-vCPU / 4-GB DigitalOcean droplet. Each site gets its own systemd unit on its own port; nginx routes traffic to the right upstream by Host header. The supporting services — secrets, databases, CI/CD, DNS/CDN — are picked once and shared across every site.
Per-site isolation via systemd + ports
Every site has a dedicated systemd service. Next.js sites listen on a port in
the 3000 range (currently 3000 through 3020 are in use); Django backends listen
in the 8000 range. Each unit runs as www-data with a fixed memory
ceiling (typically 256–512MB), automatic restart, and journal logging keyed
by service name. Adding a new site is a matter of picking the next free port,
dropping in a unit file, and writing an nginx server block — about ten minutes
of work that's now well-trodden.
nginx as the per-domain dispatcher
A single nginx instance handles SSL termination (via Let's Encrypt) and routes
each domain to its corresponding upstream. The configs live alongside their
projects in source control and get copied into /etc/nginx/sites-available/
during each project's Buildkite deploy. Because nginx is the dispatcher, no
single bad deploy can take down the whole portfolio — a misconfigured upstream
breaks one domain, not all of them.
Doppler for per-project secrets
Each site's DATABASE_URL, API keys, OAuth credentials, and
analytics IDs live in a per-project Doppler config. The systemd unit invokes
doppler run --project pa-<slug> --config prd -- <cmd>,
which injects the right env at process-start time. Rotating a secret is a
single Doppler dashboard action plus a service restart — no editing
.env files on disk, no per-droplet rsync.
Buildkite for CI/CD
Each repo has its own .buildkite/pipeline.yml. The CI agent runs
on a separate small droplet (the "ci-droplet"); deploys SSH into prod, run
git pull, install deps, run migrations, restart the relevant
service, and purge Cloudflare cache. The deploy gate is manual — no automatic
prod pushes — so a bad merge to main can be paused before it ships.
Databases: shared where it fits, dedicated where it doesn't
Most portfolio sites share a Supabase Postgres with one schema per project
(civicsmatter, namebaybay, etc.). For sites with
different scaling characteristics or single-tenant constraints — Purcell
Analytics itself runs on its own DigitalOcean Postgres — a dedicated database
gets provisioned. The decision is per-site, made when the site is built, and
not revisited later unless growth demands it.
Cloudflare in front of everything
Every domain is on Cloudflare for caching, WAF, and DDoS protection. Cache purges happen during each project's deploy via the Cloudflare API. The same Cloudflare account handles all 14+ zones — one set of credentials, one set of rules, replicated.
Outcomes
A few honest things that are true about this setup:
- The portfolio runs on a single $24/month droplet plus the supporting services. Adding the 14th site didn't require a new server.
- Adding a 15th or 16th site is a half-day of work, not a week. The pattern is well-grooved enough that the friction is on naming and content, not infrastructure.
- Operational time is bounded. Cert renewals are automatic; deploys are uniform across projects; secrets management is centralized. Time goes to building features, not running infra.
- Failure modes are localized. A broken deploy on one site doesn't take the others down. nginx and systemd's blast radii are exactly the right size.
This isn't the right shape for every team. A SaaS company with millions of users has different constraints. But for a small consulting practice running its own stack across many properties, the shared-infrastructure pattern is strictly better than 14 separate droplets — and the math doesn't change much even when you scale up to 25 or 30 sites on the same box.
The stack
The portfolio runs on DigitalOcean droplets (one for prod, one for CI), nginx for SSL + dispatch, systemd for per-service isolation, Doppler for secrets, Buildkite for CI/CD, Supabase Postgres (multi-schema) for shared databases, DigitalOcean Postgres for sites that need single-tenant, Cloudflare for caching/WAF/DNS, and Let's Encrypt for SSL.
If you're running multiple sites or properties and finding that infra overhead is starting to crowd out building, this is the pattern that generalizes — and it's exactly the kind of consolidation work we do.