Infrastructure · Incident & Resilience

Hosting resilience & redundancy — why the server died, twice

On 19 July 2026 the boom-hosting droplet ran out of memory and CPU and took nine client sites offline. Five days earlier the other droplet did the same thing. This is the post-mortem, the fix, and — the part that actually matters — an honest ladder of redundancy options with what each one costs and what it genuinely buys you. Includes the wedged-server runbook so the next one takes ten minutes instead of an hour.

Done / live In progress Recommended next Open gap

🔥 The incident — 19 July 2026

Droplet ubuntu-boom-hosting-nyc1 · 24.199.95.101 · NYC1 · Ubuntu 24.04 LTS

99.3%
CPU, last hour
96.8%
Memory, last hour
9
Client sites down
32 min
Age of last backup

Timeline

Droplet climbs toward saturation over an unknown window. No alert fires — none were configured. This is the single most expensive detail in the whole incident.
Discovery
Outage noticed by eye, on the DigitalOcean dashboard. Not by a monitor, not by an alert. Nine client sites had already been unreachable for an unknown period.
Diagnosis
Box replies to ping but SSH fails with Connection timed out during banner exchange. Kernel and NIC alive; sshd cannot fork or allocate to complete a login. That signature is memory exhaustion, not a network fault.
Recovery
No SSH means no clean shutdown. Powered off from the DO control panel — graceful attempt stalls (the OS is too starved to answer), then DO force-cuts after its timeout. Resize to a larger plan, power on. Containers with restart policies self-recover.
Second occurrence in five days. On 14 July the Supabase droplet (159.65.235.140) went down the same way — a Coolify next build (a Rankee webhook deploy) ran on the box hosting the Coolify control plane, drove load average to 167, and took the shared Supabase down with it for about an hour.

Mitigations were applied that afternoon — concurrent builds cut 2→1, a NODE_OPTIONS=--max-old-space-size=1024 build cap on the three apps, and swap raised 2→4 GB. But they were applied to that droplet only. boom-hosting never got the same treatment, and the durable fix — getting builds off production entirely — remained open. Same class of failure, different box. See Coolify setup guide.

🎯 The key distinction: backups didn't fail

This is the idea the whole document hangs on. Getting it wrong means spending money on the wrong fix.

✅ Durability — worked perfectly

"Can I get my data back?"

DO intra-daily backups were enabled and the most recent snapshot was 32 minutes old when the box wedged. Zero data was lost. Zero data was ever at risk. The shared Supabase Postgres everything depends on lives on a different droplet and was never touched.

❌ Availability — failed completely

"Are my sites up right now?"

Nine client sites served errors for an unknown duration. No amount of additional backup would have prevented one second of that. Availability is bought with monitoring, headroom, and redundancy — three different things from backups.

So when the question is "how do I stop this happening again?", more backups is the wrong answer. You already have good backups. What you're missing is (1) an alarm, (2) headroom, and (3) somewhere for traffic to go when a box dies. Sections 1–4 below, in that order — cheapest and highest-value first.

🗺️ What actually runs where

You cannot reason about redundancy without knowing the blast radius of each box. Two droplets, one managed platform.

HostAddressRunsIf it diesRedundancy today
boom-hosting 24.199.95.101 9 client Next.js sites (bm-portal, bmbb / haw / rar / rlg-vercel, eric-doerr, haw-portal, posttee, untouchables) · Traefik · MediaMTX camera streaming · Wakapi · Coolify builds 9 client sites + all camera streams offline None
Supabase droplet 159.65.235.140 Self-hosted Supabase (db.boommedia.us) · Coolify control plane · Rankee Every app's database. Ecosystem-wide outage. None
Vercel managed 11 SaaS apps Vercel's problem, not yours — no single box to fail Built in
Read that table again. Both self-hosted boxes are single points of failure with no redundancy, and one of them holds the database for the entire ecosystem. The Supabase droplet is the more dangerous of the two — and it's the one that already failed once, on 14 July. Note also that several site names literally end in -vercel: they were moved off a platform with no single point of failure onto one that has exactly one.

⬆️ The immediate fix — resize

What was undersized, what it went to, and the one checkbox worth knowing about.

2 vCPU / 4 GB
$28/mo · 80 GB
Saturated at 99% / 97%
4 vCPU / 8 GB
~$48–56/mo · Premium AMD
Doubles both constraints
Headroom
Buys time — does not
fix the root cause

The original 4 GB was carrying nine Next.js apps, Traefik, MediaMTX transcoding live camera streams, Wakapi, and Coolify builds. A single next build routinely wants 2–4 GB on its own. This was not a leak to hunt down — the box was genuinely oversubscribed from day one.

Why 4 vCPU and not just more RAM. The box was pinned at 99.3% CPU and 96.8% memory. The tempting $42/mo option is 2 vCPU / 8 GB — it fixes memory and leaves the CPU ceiling exactly where it was. MediaMTX transcoding is CPU-bound and bursty; next build pegs every core it can reach. Roughly $6/mo more buys double the CPU. Take it.

The "downscale anytime" checkbox

DigitalOcean resizes are permanent by default — grow the disk and you can never scale back down. Ticking "Downscale anytime by keeping the storage size fixed (80 GB)" preserves the ability to shrink later. It does not reduce the price, so it is free optionality. Tick it.

The one trade-off: you stay capped at 80 GB of disk. Across nine apps, Docker images and build caches accumulate fast, so pair this with a periodic docker system prune. Disk was not a factor in this outage, but it is the next thing to creep up on you.

🔔 Layer 1 · Detect — the highest-value hour you will spend

You found out by looking at a dashboard. That is the failure to fix first, and it is nearly free.

Inside the box — DO Monitoring

Free · already available on the droplet · ~10 min

Droplet page → Monitoring → create alert policies. These catch the slow climb before the wedge:

  • CPU > 80% for 5 minutes
  • Memory > 85% for 5 minutes
  • Disk > 80%
  • Deliver to email + Slack, not email alone

Outside the box — uptime checks

UptimeRobot free tier or Better Stack · ~20 min

Monitoring that runs on the server cannot tell you the server is gone. External checks can:

  • One check per client site (all nine)
  • One per camera / WHEP endpoint
  • 60-second interval, alert after 2 failures
  • SMS or push — this is the one that wakes you
If you do nothing else in this document, do these two. Combined they cost roughly $0 and half an hour, and they convert "a client tells me my server is down" into "I knew twenty minutes before it happened." Every other layer below is more expensive and buys less.

🛡️ Layer 2 · Prevent — stop building on production

Three changes, in descending order of value. The first one is the actual root cause of both outages.

Fix 1

Move builds off the production box

Open since 14 Jul

This is the root cause of both incidents. A next build competing with live traffic for 2 shared vCPUs is what drove load to 167 on the Supabase droplet and 99.3% here. Resizing buys headroom; it does not stop a build from eating the box during the next deploy.

Two options: point Coolify at a dedicated build server, or — cheaper and better — build the image in GitHub Actions, push it to a registry, and have Coolify pull the finished artifact. Production then never compiles anything; it only runs containers.

EffortHalf a day
Cost$0 (Actions free tier)
PreventsBoth incidents to date
Fix 2

Verify and size swap

Recommended

Unverified on boom-hosting — SSH was unreachable during the incident so the swap configuration could not be checked. The Supabase droplet was raised to 4 GB on 14 July; whether boom-hosting ever received the same treatment is unknown. Run free -h and swapon --show first, then size to 4 GB if it's short.

Swap turns "the server is gone and I can't even SSH in" into "the server is slow but I can log in and fix it." The entire reason this became a control-panel emergency rather than a five-minute SSH fix is that no shell was reachable — on 14 July, with swap present, the same failure was resolved by SSHing in and killing the build PID.

# 4 GB swapfile, survives reboot
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

# prefer RAM, use swap only under real pressure
sysctl vm.swappiness=10
echo 'vm.swappiness=10' >> /etc/sysctl.conf
Effort5 minutes
Cost$0
PreventsTotal lockout
Fix 3

Cap each container's memory

Recommended

Without limits, one runaway app starves the other eight and takes sshd down with it — which is exactly what locked you out. With limits, the offender gets OOM-killed by Docker and restarts; everything else stays up and you keep your shell.

# per service, in docker-compose / Coolify resource settings
mem_limit: 768m
memswap_limit: 1g
cpus: 0.75

# plus a global cleanup cron — reclaims disk from old images
docker system prune -af --filter "until=168h"
Effort1–2 hours
Cost$0
PreventsBlast radius spread

♻️ Layer 3 · Recover — make the bad day short

Assume it happens anyway. How fast can you be back, and do you actually know that number?

Have it

DO snapshots

Intra-daily backups are enabled and working — 32 minutes old at the time of failure. Keep them. This is your floor and it held.

~12 h
Interval
Live
Status
Missing

A tested restore

Per the existing DR plan, a restore drill has never been run. A backup you have not restored is not a backup — it is a hope. One afternoon: restore last night's snapshot to a throwaway droplet, confirm it boots, time it, destroy it.

Unknown
Real RTO
Never
Last drill
Partial

Offsite copy

DO snapshots live inside DO. If the problem is ever the account rather than the droplet, offsite is what saves you. Backblaze B2 is already chosen — bucket boom-media-backups — but scripts were never installed on the droplet.

B2
Target
Not wired
Status
The number you don't have is RTO. You know your data is safe (RPO ≈ 12 h, better in practice). You have no idea how long it takes to get a dead box back into service, because you've never measured it. That number is what you'd quote a client, and right now you'd be guessing. See Ecosystem Data Protection and Backblaze B2 guide.

🪜 Layer 4 · The redundancy ladder

"Redundant servers" is not one thing — it's a ladder, and each rung costs real money and real complexity. Here is the honest version, including which rungs are not worth it for Boom.

Level 0

Single box, no monitoring

Where you were

One droplet, no alerts, no swap, builds on production. Any failure is a full outage of unknown duration, discovered by luck. This is what produced two incidents in five days.

Extra cost$0
ComplexityNone
Downtime per incident1 h+, undetected
Level 1

Single box, monitored and hardened

Do this week

Same one box, plus: alerts, external uptime checks, swap, container limits, builds moved off. You still have a single point of failure — but you find out early, you can always SSH in, and the common causes are gone. This is where the vast majority of the value is, and it's essentially free.

Extra cost~$0/mo
ComplexityLow
Downtime per incident~10 min, detected
Level 2

Managed platform for anything stateless

Strongly recommended

Move the nine client Next.js sites back to Vercel. There is no box to run out of memory, no OOM, no 2am power cycle, and deploys don't compete with live traffic because builds happen on Vercel's infrastructure. For static/SSR Next.js sites the cost is frequently near zero.

Keep the droplet for what genuinely needs a persistent server: MediaMTX camera streaming, Wakapi, and anything holding long-lived processes or state. This shrinks the blast radius of a droplet failure from "nine client sites" to "cameras and internal tooling."

Extra cost~$0–20/mo
ComplexityLow — you've done it before
Downtime per incidentEffectively none
Level 3

Two droplets + load balancer

Only for what can't leave

Two identical droplets behind a DO Load Balancer, health-checked, traffic drains automatically from a sick node. Real HA for self-hosted services. The catch: this only works cleanly for stateless workloads. Anything holding local state (Wakapi's DB, MediaMTX sessions) needs that state externalised first, which is the actual work.

A cheaper cousin: Reserved IP + a warm standby. One extra droplet, kept current, and you remap the IP manually when the primary dies. Minutes of downtime instead of seconds, at roughly half the cost and a fraction of the complexity.

Extra cost+$12 LB & +$48 droplet
ComplexityHigh — state must move
Downtime per incidentSeconds
Level 4

Multi-region / managed database HA

Not yet

Failover across regions, managed Postgres with standby and PITR. The one piece of this worth taking early is moving the shared Supabase Postgres off the self-hosted droplet onto DO Managed Postgres — that box is a single point of failure for the entire ecosystem and it already failed once. Full multi-region is overkill at your scale.

Extra cost+$60–200/mo
ComplexityVery high
Downtime per incidentNear zero
The honest verdict. Levels 1 and 2 give you roughly 90% of the available benefit for roughly 5% of the cost and effort. Level 3 is worth it only for services that genuinely cannot move to a managed platform — and for those, the warm-standby variant is usually the right trade. Level 4's single valuable piece is getting the shared database off a self-hosted box. Do not build a load balancer before you've built an alert.

🚫 The other failure mode — losing the account, not the box

Everything above assumes a server fails. This section is about the provider cutting you off. It's rarer, far worse, and no amount of server redundancy helps.

A load balancer and two droplets protect you from hardware failure. They protect you from nothing if the DigitalOcean account itself is suspended — because both droplets, the load balancer, the snapshots and the reserved IP are all inside the account that just got locked. Account-level failure takes out every redundancy layer simultaneously.

How accounts actually die

CauseRealistic?WarningWhat survives
Card declines / expiresMost likely by farEmails first, then grace period, then suspensionEverything — if you catch the emails
Free-tier / plan limit hitPlausible on VercelUsage warnings, then throttle or overageData. Usually degrades before it stops.
Abuse / ToS flagUncommon but abruptOften little to noneSometimes nothing accessible
Account compromisePreventableNoneWhatever is offsite
Provider outage (region)HappensStatus pageEverything — it comes back
Be honest about the ranking. The dramatic scenario is a ToS ban. The one that actually gets people is a card that expired while the billing emails went to an address nobody reads. Cheap to prevent, and it deserves more attention than it usually gets.

Defences, cheapest first

A1

Make billing failure impossible to miss

Do today · $0

A backup payment method on both DO and Vercel. Billing alerts to an address you actually read plus Slack. Confirm the account contact email isn't one that forwards into a folder you ignore. This is fifteen minutes and it removes the single most probable cause of total account loss.

A2

Lock the account down

Do today · $0

2FA on DO, Vercel, GitHub, Backblaze and the domain registrar. The registrar matters more than people expect — losing DNS control is functionally identical to losing hosting, and it's the hardest to undo. Turn on registrar lock while you're there.

A3

Keep a rebuild kit outside every provider

Open — the real gap

If the DO account vanished tonight, could you rebuild on another host? That depends entirely on whether these four things live somewhere else:

  • Code — on GitHub, i.e. a different provider than DO. Already true
  • Database dumps — Backblaze B2, bucket boom-media-backups. Chosen, but scripts were never installed on the droplet. Gap
  • Secrets — currently .env.local files in repos, not backed up anywhere. Doppler is provisioned but unwired. Without secrets, code and data still don't get you a running system. Gap
  • DNS — at the registrar, independent of DO. Keep a written record of every record so it can be recreated. Partial

Three of those four are incomplete. This is the highest-value work in this section — and note it's the same work as offsite backups generally, so it isn't a separate project.

A4

Spread across providers

Partly free

You're already better placed than you might think: code on GitHub, backups going to Backblaze, DNS at the registrar, 11 SaaS apps on Vercel, media on Bunny. A DO account loss would hurt badly but would not be extinction-level, because DO doesn't hold your only copy of anything except the running servers.

Moving the nine client sites back to Vercel improves this again — the same change recommended at Level 2 for a completely different reason. It splits your hosting across two providers so that no single account holds everything customer-facing.

A5

Warm standby at a second host

Probably not worth it

A parallel environment at Hetzner or similar, kept current. Genuine protection against total DO loss — and a permanent second copy of everything to maintain, patch and pay for. At your scale the cost and ongoing drag outweigh the risk. A tested rebuild-from-B2 procedure gets you most of the protection for a fraction of the burden.

On "that cannot happen"

100% uptime isn't purchasable at any budget. AWS, Google and Cloudflare all have multi-hour outages on their record, with engineering budgets larger than most countries'. Anyone selling you "it can never go down" is selling something.

What is achievable, and what's worth aiming at:

That is a realistic target and you're a few free afternoons from most of it. The gap between "goes down sometimes, briefly, and I know immediately" and "never goes down" costs more than the entire business spends on infrastructure — and buys less than fixing the alerting, the builds, and the offsite secrets.

Priority within this section: A1 and A2 today (30 minutes, free, kills the most probable cause). A3 this month — it's the same work as finishing the B2 and Doppler items already open elsewhere. A4 comes free with the Vercel migration. Skip A5.

🏗️ Recommended target architecture

Where this lands if you take Levels 1 and 2 plus the one Level 4 piece.

WorkloadTodayTargetWhy
9 client sitesboom-hosting dropletVercelStateless. No box to die, no build contention, near-zero cost.
11 SaaS appsVercelVercel — no changeAlready correct.
MediaMTX camerasboom-hosting dropletDroplet, monitored + cappedNeeds persistent processes. Can't be serverless.
Wakapiboom-hosting dropletDroplet, monitored + cappedInternal tooling. Low stakes if briefly down.
BuildsOn production dropletsGitHub Actions → registryRoot cause of both outages.
Shared PostgresSelf-hosted, single dropletDO Managed Postgres + PITRSingle point of failure for the entire ecosystem.
Secrets.env.local in reposDoppler (already provisioned)Not currently backed up anywhere.

📋 Runbook — server unreachable

Follow this next time instead of improvising. Written from what actually worked on 19 July.

① Diagnose — is it the network or the box?

# does it answer at all?
ping 24.199.95.101

# can you get a shell?
ssh -o ConnectTimeout=15 root@24.199.95.101 \
    "uptime; free -h"
  • Ping fails → network or droplet is genuinely down. Check DO status page.
  • Ping works, SSH says banner exchange → memory exhaustion. The box is alive but starved. This was 19 July.
  • SSH works but slow → you got lucky. Go straight to step 3 and fix it live.

② Try the Web Console before forcing

The DO Web Console is out-of-band — serial/VNC, not SSH — so it can work when SSH is wedged. Worth 60 seconds before you hard-cut.

systemctl stop docker
poweroff

Stopping Docker first lets containers flush and exit cleanly. If the console never gives you a prompt, close it and move on — the box is too starved.

③ Power off from the panel

  1. Actions → Power Off, type the droplet name to confirm.
  2. DO tries graceful first. On a starved box this will stall — expected, not a problem.
  3. Wait ~5 minutes for DO's timeout to force it.
  4. Still stuck past 7–8 min? Refresh the page — the status badge goes stale. Then Actions → Power Cycle (unconditional hard reset), then Power Off again — it shuts down cleanly once memory pressure clears.

Nothing is being damaged while it sits there. The box is too starved to write meaningfully to disk. Waiting is safe.

④ Resize and bring back up

  1. Wait for the badge to read Off. Resize stays greyed out until then.
  2. Settings → Resize. Tick "Downscale anytime" to stay reversible.
  3. Basic → Premium AMD, pick the target plan.
  4. Resize Droplet — 2–5 minutes.
  5. Power On, wait ~2 minutes to boot.
  6. Containers with restart policies self-recover. Verify all nine sites and the camera streams before calling it done.
Before touching anything destructive, check what's at risk. On 19 July the calculus was: the shared Postgres is on a different droplet, the Next.js apps are stateless and rebuild from git, and there was a 32-minute-old backup. That made a forced shutdown an acceptable risk. The same call on the Supabase droplet would be very different — that one holds the database for everything.

💵 What each layer costs

Monthly deltas against the pre-incident baseline.

ItemLayerCost / moBuys you
DO Monitoring alerts1 · Detect$0Warning before the wedge
UptimeRobot free tier1 · Detect$0You learn it's down before a client does
Swap + container limits2 · Prevent$0Degrade instead of lock out
GitHub Actions builds2 · Prevent$0Removes the root cause of both outages
Droplet resize 4→8 GB, 2→4 vCPU2 · Prevent+$20–28Real headroom Done
Restore drill3 · Recover~$1A real RTO number instead of a guess
B2 offsite sync3 · Recover~$1–5Survives losing the DO account itself
Move 9 sites to Vercel4 · Redundancy~$0Removes the largest single point of failure
DO Load Balancer + standby4 · Redundancy+$60Seconds of downtime — only if state moves first
DO Managed Postgres4 · Redundancy+$60–200Ecosystem DB stops being one box
Recommended path (Levels 1–2)≈ +$25/mo~90% of the available benefit
The recommended path is almost entirely free. Nearly everything that matters — alerts, uptime checks, swap, container limits, moving builds off production, moving client sites back to Vercel — costs nothing but time. The only committed spend is the resize you've already done.

Action items

In priority order. The first two are the ones that would have changed the outcome on 19 July.

DONE · Resize boom-hosting to 4 vCPU / 8 GB. Completed 19 July during the outage. Downscale-anytime enabled to stay reversible.
TODAY · Configure DO Monitoring alerts — CPU >80%, memory >85%, disk >80%, to email + Slack. Ten minutes. This is the single highest-value item in the document.
TODAY · External uptime checks on all nine client sites plus camera endpoints, 60-second interval, SMS alerting.
THIS WEEK · Move builds off production. Open since the 14 July incident and the direct cause of both outages. GitHub Actions → registry → Coolify pulls.
THIS WEEK · Add 4 GB swap + container memory limits on both droplets. Five minutes and an hour respectively.
THIS MONTH · Run a restore drill. Never been done. Restore to a throwaway droplet, time it, destroy it — you need a real RTO number.
THIS MONTH · Decide Vercel vs droplet for the nine client sites. Prior infrastructure review already recommended keeping apps on Vercel; this outage is the evidence for that call.
OPEN · Shared Supabase Postgres is a single point of failure for the whole ecosystem, on a box that already failed once. Managed Postgres migration is the highest-stakes item here, even if it isn't the most urgent.
OPEN · Wire Doppler. Provisioned but repos still on .env.local. Secrets are currently not backed up anywhere.
TODAY · Backup payment method + billing alerts on DO and Vercel, to an address you actually read. Expired-card suspension is the most probable route to total account loss, and this is 15 free minutes.
TODAY · 2FA everywhere — DO, Vercel, GitHub, Backblaze, and the domain registrar. Enable registrar lock. Losing DNS control is as bad as losing hosting and much harder to reverse.
SECURITY · Rotate the keys pasted into chat — Twilio auth token, HeyGen, Bunny API. Still outstanding. Install doctl and run doctl auth init locally rather than sharing a DO token that can destroy droplets.