Skip to content
← Blog

June 23, 2026

Convenience Has a Price

I moved Flashfood Deal Finder off Google Cloud Run and Firestore onto a self-hosted Docker setup with MongoDB. Here's why, and what I learned about the hidden assumptions in managed infrastructure.

I built Flashfood Deal Finder as a side project — no revenue, no monetization plan, just a tool I wanted to exist. It helps people find discounted groceries on Flashfood across different cities, stores, and categories. The whole thing runs on Next.js — Firestore `onSnapshot` for a real-time deal feed, composable filters, infinite scroll, Korean/Japanese/French/Chinese support, dynamic Open Graph images. It got bigger than I expected. I'm kind of proud of it, which made what came next more annoying.

And then I checked the billing dashboard and it wasn't the $0–3/month situation I'd been used to.

The initial setup was actually great

Cloud Run scales to zero when nobody's using it, so I wasn't burning money in idle time. Cold starts were acceptable — usually under a second since I kept the image lean. For a low-traffic tool I wasn't sure anyone would use, the tradeoff felt fine.

Firestore's `onSnapshot` is genuinely magic — you subscribe and stuff updates in real time, the SDK handles reconnection and local caching, and latency for Canadian users on `northamerica-northeast1` was typically under 100ms. The Spark free tier gives you 50,000 reads per day and 20,000 writes, which felt like a lot at first. Setup was maybe a weekend of fighting with IAM permissions and then it mostly worked.

At some point I sent the link to a few people and they actually used it, which was weird and good.

Traffic is a good problem until it isn't

The problem with `onSnapshot` is how it counts reads. Every deal card is a document read, and the listener re-fires on any change to any document in the query. With a live grocery feed that updates throughout the day, a single active session can rack up hundreds of reads in minutes. Once I crossed the Spark free tier limit, I was on Blaze pay-as-you-go at $0.06 per 100,000 reads — which sounds cheap until you multiply it by active sessions. Cloud Run added its own line items too. My monthly bill was landing somewhere I hadn't planned for, trending upward with no natural ceiling.

I put off dealing with it for probably longer than I should have, then eventually just accepted I had to move it.

If I was charging money for this, fine — that's just a scaling cost you figure out. But I'm not. There's no subscription page, no Patreon, nothing. I built it because I thought it was useful. So the money just comes from me, indefinitely, for a project that exists as a public service to people finding deals on groceries. I don't resent that, but I also can't sustain it.

The self-hosting decision

I already have a server running other things. The obvious move was Docker — containerize the app, run it on my own hardware using Docker Compose with the Next.js app and MongoDB as separate services, fronted by Nginx handling TLS termination. Cloud costs: effectively zero beyond electricity and bandwidth I'm already paying.

The harder question was the database. PostgreSQL was briefly a contender, but MongoDB's document model is a closer conceptual fit for what Firestore stores, and the migration path for nested data is more straightforward. I exported the Firestore data with `firebase-admin` and wrote a one-time transform script to flatten the nested collections into a single `deals` collection.

The part I'm still working out: MongoDB change streams — the closest thing to `onSnapshot` — require a replica set, even for a single-node deployment. A standalone `mongod` without `--replSet` has no change streams at all. So the real-time feed behavior needs rethinking. My current plan is Server-Sent Events from a Next.js API route polling MongoDB on a short interval. Less elegant than `onSnapshot`, but it gets the job done without WebSocket infrastructure. I'm a little nervous about whether it'll feel as snappy. We'll see.

What I actually think about this

I don't think managed services are bad. For most things, especially when you're moving fast or don't want to think about ops, they're genuinely great. Cloud Run and Firestore let me build something pretty involved without drowning in infrastructure.

But nobody told me to think about what happens when the project grows and never makes any money. That's on me. I just kind of assumed it would stay cheap, and it didn't.

Self-hosting is more work. I'm now responsible for MongoDB backups, container restarts on crash, TLS cert renewal via Certbot, keeping the host OS patched — none of which Cloud Run ever made me think about. Something will break at a bad time and it'll be my problem to fix.

But for a project that exists because I think it should exist — not because it makes sense as a business — it's the only thing that actually makes sense. I'd rather deal with the ops than quietly kill the project because the bill got too high.

Anyway. Docker container, MongoDB, let's see how it goes.