What It Costs to Run 4 Apps on My Own Servers

📆 · ⏳ 9 min read · ·

The short version

I run four products on my own: Hydrafetch ↗️, DataHyena ↗️, CatchIntent ↗️ and Eternal Vault ↗️. Paying customers, background workers, scheduled jobs, real databases. The whole thing.

All four of them run on one server I set up myself. The total bill is about $45 a month.

Put the same setup on a managed platform and you’re looking at somewhere between $360 and $960 a month.

Every time I mention this, the reply is some version of “okay, but what’s the catch?” Fair question. So this post is the honest answer: the full bill, what you give up for that price, how I cover it, and the things that broke along the way. If you run a small SaaS and your hosting bill has started to hurt, this is roughly what the other side looks like.

The bill

WhatPer month
One server: 8 dedicated cores, 16 GB RAM, 500 GB NVMe$40
Off-site backup storage (S3-compatible, like R2 or B2)a few dollars
CDN, DNS and DDoS protection (Cloudflare)$0
Uptime checks and alerts$0
Total~$45

Domains and email are extra, but you pay for those wherever you host, so I’ve left them out.

That’s it. That’s the bill.

The part people find hard to believe is that the server isn’t even sweating. Every app gets hard limits on CPU and memory, and there’s still room to spare.

Here’s what one app, DataHyena, gets:

ContainerCPUMemory
Web1.52 GB
Worker22 GB
Postgres23 GB
Redis0.25512 MB

That one app holds over 4 GB of Postgres data: 110k+ companies, 58k funding rounds, 65k hiring events and 200k+ raw documents, with vector search on top. Its worker pulls in up to 2,000 new documents a day. This isn’t a todo app, and it’s not even the biggest thing on the box.

What the managed version would cost

To keep it fair, I priced the same shape of app on the usual platforms: a web service, a background worker, a Postgres database big enough for the data, and a small Redis.

For DataHyena alone, that lands somewhere around $90 to $240 a month, depending on the platform and how generous you are with the database. And it’s always the database. Managed Postgres gets expensive fast once you need more than a couple of gigabytes of RAM, and vector search wants RAM.

Do that for four apps and you’re at $360 to $960 a month. Against $45.

That gap is the whole reason I do this. But it isn’t free money either. A managed platform does real work for you, and when you move off it, that work doesn’t disappear. Somebody has to pick it up.

What $45 a month doesn’t buy you

When you pay a platform, you’re not just paying for compute. You’re paying for a long list of things you never have to think about. Here’s that list, and what I do instead of paying for it.

Backups that actually restore

Every database gets two kinds of backup:

  • Continuous archiving of the write-ahead log with wal-g ↗️, so I can restore to any point in time, not just last night. A bad migration at 4pm costs me minutes of data, not a whole day.
  • A daily dump, kept for a week. A boring fallback that doesn’t depend on anything clever, which is exactly why it’s there.

Both get encrypted with GPG before they leave the server and land in off-site object storage. The jobs are staggered too, so the databases aren’t all compressing and uploading in the same minute.

I’ve written up how dumps and point-in-time recovery work if you want the details.

And yes, they’re tested. More on that below.

Deploys and rollbacks

Deploys go through Kamal ↗️. A push builds a new image, starts it, waits for it to pass a health check, and only then switches traffic over. If the new version doesn’t come up, the old one just keeps serving.

Rolling back is one command. I’ve needed it more than once, and every time I’ve been glad it was one command and not a fix-forward at midnight.

Security

  • SSH isn’t open to the internet at all. It’s only reachable over Tailscale, and it’s key-only with root login off.
  • The firewall only accepts web traffic from Cloudflare’s IP ranges, so nobody can skip the CDN and hit the server directly.
  • Databases are never exposed. They sit on a private Docker network that only their own app can reach.
  • Security updates install on their own, and fail2ban bans anyone guessing at the door.

Most of this is in my first 30 minutes on a new VPS checklist. None of it is fancy. It just has to be done, every time, without skipping a step.

Monitoring

Metrics, logs and traces from every app go into one Grafana stack. On top of that, an outside uptime check pings me the moment something is down, so I hear about it before a customer does.

And for the quieter stuff, like a stuck job or a query that got slow, I built a watchman for my servers that only speaks up when it matters.

The whole server, written down

If I could only keep one thing from this list, it’d be this one.

The entire server, from users and firewall rules to Postgres settings, backup schedules and monitoring agents, lives as Ansible code in a git repo. I started doing this for my homelab years ago, and it’s the best habit I ever picked up.

If the server died tonight, I’d rent a new one, run the playbook, restore the databases, and be back in about an hour. Not a week of trying to remember what I installed and why.

PROMOTED Built & launched by me

Company-event signals as one clean API

Datahyena logo

Datahyena delivers funding rounds, acquisitions, and executive moves as clean, typed records. Every signal is deduplicated and resolved to canonical companies and investors, ready for your GTM, research, or data workflow.

Get 50 free credits
No card required

Things that went wrong

None of this was smooth. Here are the ones worth telling.

Every database was running on defaults, for months

I had a Postgres tuning step in my Ansible setup. Memory settings, planner settings, all of it sized for the box. I wrote it, reviewed it, and moved on.

It never ran.

A mistake in how the settings were wired meant every database on the server was running on stock Postgres defaults, the ones meant for a laptop. Including random_page_cost = 4.0, which tells Postgres to assume it’s reading from a spinning hard disk. On NVMe.

Nothing was broken, so nothing alerted. Queries were just slower than they should have been, quietly, everywhere. I only found out when one query hit a 30-second statement timeout and I went digging.

The fix was a few lines. The lesson was bigger.

đź’ˇ
Check that your config is live

Not just that it exists in a file somewhere. Now my setup reads back the running values after it applies them, and complains if they don’t match.

One restart took everything down

All four apps sit behind one shared proxy that routes traffic to each of them. That’s a big part of why this is so cheap.

Which I learned the fun way. The first time I restarted that proxy without thinking, every app went down at once. Only for a few seconds, but every customer of every product saw it at the same moment.

Now proxy changes get treated like deploying every app at once: planned, quick, and never in the middle of the day. It also nudged me to write down exactly how I’d add a second box when the time comes, which brings me to the next bit.

A deploy that blocked every deploy after it

A deploy got interrupted halfway through and left behind a process that the next deploy saw and politely waited on. Forever. Every deploy after that just hung, and nothing told me why.

It took far longer to find than to fix. There’s now a check for leftovers before every deploy, and it’s in my runbook so future me doesn’t have to rediscover it.

The migration was the real backup test

Earlier this year I moved everything from my old server to a new one. I didn’t copy disks across. I restored every single database onto the new server from its off-site backups, exactly the way I would after a disaster.

It worked. And honestly, it was the most reassuring thing I did all year.

Until you’ve restored a backup, you don’t have backups. You have files.

One server today, more when it’s needed

One box is a choice for where my products are right now, not a ceiling. The nice thing about building it this way is that nothing here is tied to a single machine, so growing it is mostly adding, not rebuilding.

  • More servers is one playbook run. Since the whole server is Ansible code, a second (or fifth) server comes up identical to the first. No snowflakes.
  • Apps spread across them with a config change. Kamal deploys to a list of hosts, not one. Add a host, put a load balancer in front, and traffic is shared. If one box goes away, the others keep serving.
  • The database gets a standby. Postgres streaming replication keeps a hot copy on another server, ready to promote. The WAL archive I already keep for backups is exactly what a new replica is built from, so that groundwork is done.
  • Busy apps move to their own box. Each app is already isolated with its own containers, limits and database, so lifting one onto a dedicated server is a deploy, not a migration project.
  • Regions are just more servers. Cloudflare already sits in front of everything, so failing over between locations is a routing change, not a re-architecture.

So the path from “one box” to “redundant across machines” is a handful of well-trodden steps, and every one of them still costs a fraction of the managed bill. I just don’t pay for that redundancy before my products need it.

Paying too much for managed hosting?

I move small teams off Vercel, Render or Heroku onto a server they control, usually for a fraction of the bill. Deploys, backups and monitoring included. Fixed price, from $2,500.

When you shouldn’t do this

I’d be lying if I said this is right for everyone.

It takes time. Setting this up properly took weeks. Now it’s maybe an hour or two a month, mostly reading alerts and keeping things patched. But someone has to own that hour.

So stay on managed hosting if:

  • Your bill is small, and moving would save less than your time is worth.
  • Nobody on the team can own the server, and you don’t want to pay someone to.
  • You’d rather pay a platform’s markup than ever think about servers, and the bill doesn’t bother you yet.

Move if the bill keeps growing a line item at a time, and most of what you’re paying for is headroom you never use. Which, from what I’ve seen, is most small SaaS apps.

If you want to try it

Start with the boring parts, not the server. If you can’t answer “when did we last restore a backup?”, you’re not ready to move. And honestly, you’re probably not as safe as you think where you are right now either.

I turned the checks I run on every setup into a free production readiness checklist. It takes five minutes and tells you what you’d lose if your server died today.

And if you’d rather not spend the next few weekends on this, I set this up for small teams, done for you, with a tested restore before I hand it over.

You may also like

  • # devops# engineering# selfhosted

    I Built a Watchman for My Servers (It Even Opens Its Own Fix PRs)

    Running a few products solo means a lot of things can quietly break while you're not looking. So I built something that watches for me, only speaks when it matters, investigates the problem itself, and hands me the fix.

  • # devops# postgres

    Database Backup Strategies - automated dumps, point-in-time recovery

    Your database just corrupted at Midnight. Here's how to set up automated backups and point-in-time recovery so you can sleep without panic.

  • # devops# postgres

    PostgreSQL on VPS - installation, configuration, security

    Your backend needs a database. Here's how to set up PostgreSQL with Docker Compose on your VPS - the way most developers actually do it in 2025.