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:
Listens for commits on my GitHub repo
Automatically builds the NestJS backend
Deploys with zero-downtime (spins up new instance, switches traffic, kills old instance)
Shows deployment status in a dashboard
The setup:
Install Dokploy on my VPS
Connect it to my GitHub repo
Configure build settings (Node.js, environment variables, ports)
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>
What this does:
Builds the React Native bundle
Creates an update manifest
Uploads it to my self-hosted Xavia OTA server
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
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:
Listens for new past questions (scraped or manually added)
Automatically updates MongoDB with new content
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:
Removed the broken update from Xavia OTA
Re-published the previous stable version
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:
Check the logs in Dokploy
Fix the issue locally
Push the fix
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.
Written while debugging to: [Dunsin Oyekan– Nagode]
Top comments (0)