DEV Community

Jonathan Mensah
Jonathan Mensah

Posted on • Originally published at jmensah.hashnode.dev

How I Handle Deployments and CI/CD for a $5/Month Production App

Running a production app with ~10,000 active users on a $5/month VPS sounds impossible. But it's not, if you're smart about your tooling.

Here's my entire deployment setup: from code commit to production, backend to mobile OTA updates, and how I handle rollbacks when things break.

Spoiler: I didn't compromise on automation. I just stopped paying for things I could self-host.

The Stack

Infrastructure:

  • $5/month VPS (2 vCPU, 4GB RAM + swap memory)

  • GitHub (free tier)

  • Dokploy (self-hosted, free)

  • Xavia OTA (self-hosted, open source)

  • MongoDB Atlas (free tier)

  • Cloudflare R2 (free tier)

Total monthly cost: $5. That's it.

Backend Deployment: Dokploy Does Everything

I used to manually SSH into my VPS, pull code, restart services, and pray nothing broke. Now I use Dokploy—a self-hosted alternative to platforms like Railway or Render.

What Dokploy does:

  1. Listens for commits on my GitHub repo

  2. Automatically builds the NestJS backend

  3. Deploys with zero-downtime (spins up new instance, switches traffic, kills old instance)

  4. Shows deployment status in a dashboard

The setup:

  1. Install Dokploy on my VPS

  2. Connect it to my GitHub repo

  3. Configure build settings (Node.js, environment variables, ports)

  4. Done

Every time I push to main, Dokploy:

  • Pulls the latest code

  • Installs dependencies

  • Builds the app

  • Deploys it

  • Notifies me if it fails

No GitHub Actions minutes consumed. No paying for CI/CD platforms. Just a self-hosted tool doing exactly what I need.

How I know if deployment failed:

The Dokploy dashboard shows:

  • Build logs in real-time

  • Deployment status (success/failed)

  • Current running version

  • Resource usage

If something breaks, I see it immediately.

Mobile Deployment: Self-Hosted OTA Updates

This is where it gets interesting.

I used to pay for Expo's OTA update service. Then I found Xavia OTA, an open-source alternative I could self-host.

What Are OTA (Over-The-Air) Updates?

For those unfamiliar: OTA updates let you push JavaScript/React Native changes to users without going through the App Store or Play Store review process.

What can be updated OTA:

  • Bug fixes

  • UI changes

  • Business logic

  • API endpoint changes

What cannot be updated OTA:

  • Native code changes (requires a full app store update)

  • New permissions

  • Version bumps that change native dependencies

This is crucial for mobile apps. Play Store reviews can take days. OTA updates deploy in minutes.

How I Set Up Xavia OTA

Xavia OTA had one limitation: it only supported one app per instance. If you had multiple apps, you needed multiple Docker containers.

I tinkered with the code and made it support multiple apps in a single instance. Now I can host updates for multiple projects without spinning up separate containers.

The deployment process:

I have a script: ./build-and-publish-app-release.sh

./build-and-publish-app-release.sh <runtimeVersion> <xavia-ota-url> <uploadKey>
Enter fullscreen mode Exit fullscreen mode

What this does:

  1. Builds the React Native bundle

  2. Creates an update manifest

  3. Uploads it to my self-hosted Xavia OTA server

  4. Users get the update next time they open the app

Example:

./build-and-publish-app-release.sh 1.2.3 https://ota.myapp.com abc123key
Enter fullscreen mode Exit fullscreen mode

Within minutes, all users on runtime version 1.2.3 get the update. No app store. No waiting.

Version Compatibility Strategy

Here's the key: all my backend APIs are backward compatible.

This means:

  • New app versions can talk to old backend APIs

  • Old app versions can talk to new backend APIs

  • I never force users to update immediately

This prevents the nightmare scenario where a backend update breaks older app versions.

Database Updates: Automated Past Questions Sync

My app serves exam past questions. These get updated regularly.

I have an automated script that:

  1. Listens for new past questions (scraped or manually added)

  2. Automatically updates MongoDB with new content

  3. Runs on a cron job

Users get fresh content without me manually updating the database every time.

What Breaks (And How I Handle It)

The OTA Update That Crashed Everything

I pushed an OTA update once. Within minutes, users started reporting crashes.

The problem: I had introduced a bug that only appeared on certain Android devices (classic React Native moment).

The fix:

Because I control the OTA server, I just rolled back the update:

  1. Removed the broken update from Xavia OTA

  2. Re-published the previous stable version

  3. Users automatically got the working version

Total downtime: ~15 minutes.

If this had been an app store update, I would've been screwed. Users would be stuck with a broken app for days while the fix went through review.

Backend Deployment Failures

Dokploy's dashboard shows me immediately if a deployment fails:

  • Build errors (syntax error, missing dependencies)

  • Runtime errors (crashes on startup)

When this happens:

  1. Check the logs in Dokploy

  2. Fix the issue locally

  3. Push the fix

  4. Dokploy auto-deploys the corrected version

If the new deployment is completely broken, I can roll back to the previous version with one click in the Dokploy dashboard.

Monitoring and Health Checks

Backend monitoring:

  • Dokploy shows resource usage (CPU, RAM, network)

  • I have Sentry for error tracking (from my previous post)

  • Simple HTTP health check endpoint (/health)

Mobile monitoring:

  • Sentry for crash reporting

  • Analytics for user sessions

  • OTA update adoption rate (I can see how many users are on each version)

If something's wrong, I know within minutes, not hours or days.

The $5/Month Reality Check

People ask: "How can you run production on a $5 VPS?"

Here's the secret: A 2 vCPU, 4GB RAM VPS is plenty for most apps.

What I run on this VPS:

  • NestJS backend

  • Dokploy (for deployments)

  • Xavia OTA (for mobile updates)

  • Nginx (reverse proxy)

  • Automated scripts (cron jobs for content updates)

The trick: I added swap memory to handle occasional spikes. The VPS rarely uses more than 60% of its resources under normal load.

I might upgrade when I deploy more applications, but for now, it's more than enough.

What I Didn't Have to Compromise On

This is important: I didn't compromise on automation because of the budget.

I still have:

  • Automated deployments (Dokploy)

  • OTA updates (Xavia OTA)

  • Zero-downtime deploys (Dokploy handles this)

  • Rollback capability (one click)

  • Real-time deployment logs

  • Error tracking (Sentry)

The only difference? I self-host the tools instead of paying for SaaS platforms.

What I saved by self-hosting:

  • GitHub Actions: $0 (would be ~$10-20/month for private repos with heavy CI usage)

  • Expo OTA: $0 (would be $29/month)

  • Railway/Render: $0 (would be $20-50/month)

  • Total savings: ~$60-100/month

What I'd Do Differently

1. Set up automated backups earlier

I should've automated MongoDB backups from day one. I do manual exports weekly now, but this should be automated.

2. Add deployment notifications

Dokploy doesn't notify me on success/failure. I should set up a webhook to Slack or Discord so I know immediately when deployments happen.

3. Implement staged rollouts for OTA updates

Right now, OTA updates go to all users at once. I should implement:

  • 10% of users get the update first

  • If no crashes/errors after 24 hours, roll out to 100%

This would've caught that Android crash before it hit everyone.

The Bottom Line

You don't need expensive DevOps tools to run a production app. You need:

  • A decent VPS ($5/month is enough)

  • Self-hosted CI/CD (Dokploy)

  • Self-hosted OTA updates (Xavia OTA)

  • Backward-compatible APIs

  • A rollback strategy

I went from paying ~$100/month (Expo OTA + managed hosting + CI/CD) to $5/month, without losing any automation or deployment speed.

The tradeoff? You have to set up and maintain the tools yourself. But if you're a solo dev or a student, that's a tradeoff worth making.


Next up: How I built a multi-app OTA update system for $0/month (and saved $199/month by ditching Expo's service).


Written while debugging to: [Dunsin Oyekan– Nagode]

Top comments (0)