Originally published at LeanStack.
Hosting a small web app does not have to start with a monthly bill. For 12 days, from October 26 to November 8, 2026, my production app – a multi-school online exam platform built with Node.js, PostgreSQL and Vue – ran on a Windows laptop at home, reachable on its own domain through Cloudflare Tunnel. Then I moved it to a free cloud VM.
Short answer: if your app is small, your users can tolerate the occasional outage, and you are comfortable doing your own backups, a home PC plus Cloudflare Tunnel costs about $3 a month and performs better than you would expect. Once real money or fixed-schedule exams depend on uptime, move to a server that never sleeps.
This guide shows exactly how my setup worked, the load numbers I measured, what it really cost, the two outages I had, and what I moved to.
Why I ended up hosting at home
I planned to use a free cloud VM, but my sign-up for Oracle Cloud's free tier was rejected. Paid VPS prices also went up across the market in 2026 (Hetzner alone changed prices several times), so I tried the cheapest option I had: the laptop I already owned.
The app was already running on that laptop for development, with portable Node.js and PostgreSQL. Going live meant installing one more program, cloudflared, and pointing the domain at it. It took an evening.
The setup at a glance
| Part | What I used | Cost |
|---|---|---|
| Server | Dell Inspiron 7460 laptop, Windows 10, Intel Core i7-7500U (2 cores / 4 threads), 8 GB RAM | Already owned |
| Runtime | Portable Node.js 22 and portable PostgreSQL (no Docker, no WSL) | Free |
| Public access | Cloudflare Tunnel (cloudflared), domain on Cloudflare DNS | Free tunnel; a .vn domain renews at about 470,000 VND (~$18) a year |
| Multi-tenant subdomains | Wildcard DNS *.yourdomain routed through the same tunnel | Free |
| Gmail SMTP with an App Password | Free (daily sending limit) | |
| Backups | pg_dump before every database migration | Free |
How Cloudflare Tunnel works (and why it beats port forwarding)
A tunnel flips the usual model. Instead of opening a port on your router and pointing DNS at your home IP, a small program called cloudflared runs on your PC and opens an outbound connection to Cloudflare. Visitors hit Cloudflare, and Cloudflare sends the request down that connection to your local app.
That gives you three things for free:
- No open ports. Your router stays closed; nothing scans your home IP.
- HTTPS handled for you. Cloudflare terminates TLS with its own certificate.
- A stable address. It does not matter if your ISP changes your home IP.
I created the tunnel in the dashboard (Zero Trust → Networks → Tunnels), copied its token, and installed cloudflared as a Windows service from an Administrator prompt, so it starts with Windows even before anyone logs in:
cloudflared.exe service install <YOUR_TUNNEL_TOKEN> In the tunnel's Public Hostname settings, both yourdomain.com and *.yourdomain.com point to http://localhost:3000. The wildcard is what lets every school get its own subdomain without touching DNS again.
Making Node.js use the whole CPU
A single Node.js process uses one CPU core. On a 4-thread laptop CPU that leaves most of the machine idle, so the app starts through Node's built-in cluster module with one worker per CPU thread:
import cluster from 'node:cluster'; import os from 'node:os'; if (cluster.isPrimary) { for (let i = 0; i < os.availableParallelism(); i++) cluster.fork(); cluster.on('exit', () => cluster.fork()); // restart a crashed worker } else { await import('./index.js'); // the normal Express server } One catch: each worker has its own in-memory cache. When an admin changes something, the other workers do not know. I solved it with PostgreSQL's built-in LISTEN/NOTIFY: the worker that writes sends a notification, and every worker clears that cache key. No Redis needed.
Two more settings mattered behind Cloudflare:
- Turn off gzip in Node. Cloudflare already compresses responses, and compressing in Node competes with password hashing for the same thread pool.
- Trust the proxy. Set Express
trust proxyso rate limits and logs see the visitor's IP, not Cloudflare's.
Real load test: 1,000 users on a 2016 laptop CPU
Exams are the worst case for my app: hundreds of students arrive in the same minute, then save answers every few seconds. My load-test script simulates exactly that: each virtual user opens the exam, saves a draft three times, submits, and opens the results and the leaderboard. The database and the load generator ran on the same laptop as the app, so the numbers are on the pessimistic side.
First run, a single Node.js process, 1,000 users arriving over 30 seconds:
| Scenario | Result |
|---|---|
| 1,000 guests taking a password-protected exam | 1,000/1,000 completed, 0 errors. p95: open exam 92 ms, save draft 100 ms, submit 95 ms |
| 1,000 students registering, then taking the exam | 1,000/1,000 completed, 0 errors, but median registration took about 9 seconds |
The surprise was where the limit sits. It is not the database or Express; it is password hashing. Each scrypt hash takes around 80–100 ms of CPU on purpose (that is what makes stolen hashes expensive to crack). On two physical cores that caps the machine at roughly 20–25 logins per second, so a burst of 1,000 logins queues up no matter how fast the rest of the app is.
Switching to cluster mode (4 workers) and replaying 1,000 student logins over 20 seconds showed the trade-off clearly:
| Hashes per worker at once | Median login | Median for every other page |
|---|---|---|
| 1 | 17 s | 60 ms |
| 2 | 13 s | 300 ms |
I kept one hash per worker: logins wait a bit longer, but students already in the exam stay fast. The practical fix is operational, not technical: ask students to log in 5–10 minutes before the exam (sessions last 7 days), and if you expect bigger spikes, you need more cores, not a faster database.
PostgreSQL itself needed only light tuning: I raised shared_buffers to 512 MB with ALTER SYSTEM (a restart is required) and pointed the app at 127.0.0.1 instead of localhost to skip a DNS lookup on Windows.
What it really costs per month
| Item | Monthly cost |
|---|---|
| Hardware | $0 (already owned) |
| Electricity | About 11 kWh (a laptop averaging ~15 W × 24 h × 30 days). At Vietnam's household rates from November 2026 (1,901–3,844 VND/kWh plus 10% VAT), that is 23,000–46,000 VND, roughly $1–2 |
| Internet | $0 extra (existing home plan; I measured about 30 Mbit/s upload, plenty for an API) |
| Cloudflare Tunnel + DNS | $0 |
| Domain | About $1.50 (~$18 a year for a .vn; a .com at Cloudflare Registrar is cheaper) |
| Total | About $3 a month |
For comparison, the cheapest VPS options I checked on November 8, 2026:
| Provider | Plan | Specs | Price/month |
|---|---|---|---|
| DigitalOcean | Basic Droplet | 1 vCPU, 1 GiB RAM, 25 GiB SSD | $6 |
| Hetzner | CPX02 (EU) | 1 vCPU, 1 GB RAM, 20 GB NVMe | €6.49 (+VAT) |
| Hostinger | KVM 1 | 1 vCPU, 4 GB RAM, 50 GB NVMe | $6.49 intro with 24-month prepay, renews at $11.99 |
None of those 1-vCPU plans would match the 4 threads of an old laptop CPU for the login-heavy workload above.
The risks nobody mentions (and the ones that hit me)
Home hosting is cheap because you are the operations team. These are the risks to plan for:
- Sleep, not power cuts, took my app down. A laptop battery is a built-in UPS, and
cloudflaredran as a Windows service while the app started from a shortcut in the Startup folder. But the laptop's power plan still allowed sleep, and it went to sleep twice: 43 minutes on the morning of October 28 and about 11 hours overnight on November 7. That is roughly 12 hours of downtime in 12 days, or about 96% uptime. Set sleep to Never on both battery and AC on day one (powercfg /change standby-timeout-ac 0andstandby-timeout-dc 0), and set Windows Update active hours so it does not reboot during exams. - Startup folder is not a service. A shortcut in the Startup folder only runs after a user logs in. After an unattended reboot, the tunnel service would come back on its own but the app would not until someone signed in. Use Task Scheduler ("At startup") or a service wrapper instead.
- Internet outages. Your uptime equals your ISP's uptime. My connection did not drop once in those 12 days, but I had no plan B if it had.
- Backups are your job. I took a
pg_dump -Fcbefore every migration and kept copies in a separate folder. A dead disk with no off-site copy means lost data. - Deploys without downtime. Building the frontend straight into the live folder causes a few seconds of 404s. I build into a new folder and swap it in.
- Terms of service. Some residential ISPs forbid running servers, and Cloudflare's terms limit serving large media through the free plan. Check both before you rely on this.
- Security. Only the app is published through the tunnel, never Remote Desktop or the database port. If you add an admin tool, protect it with Cloudflare Access.
When to move to a VPS (or a managed platform)
Stay on the home PC while the app is a side project or an early pilot. Move when any of these become true:
- People pay you, or exams run on a fixed schedule. An hour of downtime now costs money or trust.
- You travel or cannot reach the PC quickly to wake it up or fix a crash.
- You need more than one machine, for example a separate database server.
The simplest next step is a small VPS: copy the same Node + Postgres setup, keep Cloudflare Tunnel (it works the same on Linux), and nothing changes for your users. A DigitalOcean Droplet or a Hetzner server is enough for an app of this size; check the CPU count if logins are your bottleneck like mine.
If you would rather not manage a server at all, platforms like Railway or Render run the app for you, and Neon offers managed Postgres with a free tier. You trade a higher monthly bill for no maintenance. I compare these options with real prices in Cheapest Way to Host a Node.js + Postgres App in 2026.
What I moved to: Google Cloud's free e2-micro
After the 11-hour outage I moved the app to Google Cloud's Always Free e2-micro VM in Oregon (us-west1): 2 shared vCPUs, 1 GB RAM and a 30 GB standard persistent disk, all inside the free allowance. Moving took a few hours, one night:
- Debian 12 with PostgreSQL 16 and Node.js 22, plus a 2 GB swap file because 1 GB of RAM is tight.
- Postgres scaled down for the small machine:
shared_buffers = 128MB,max_connections = 40. - The same Cloudflare Tunnel, so the VM has no open web ports either. Switching over was: uninstall the tunnel service on the laptop, restore the last
pg_dumpon the VM, start the tunnel there.
With the app, Postgres and cloudflared running, free -m on the VM shows 519 MB used of 969 MB, with 32 MB of swap in use and 4.8 GB of the 30 GB disk used. The trade-offs: the e2-micro's CPU is shared and much weaker than the laptop for a burst of logins, Oregon is far from my users in Vietnam, and only 1 GB of egress per month is free, so Cloudflare's caching matters. For now, an app that never sleeps beats an app that is fast but sometimes offline.
My verdict
For a small Node.js + PostgreSQL app, a home PC with Cloudflare Tunnel is a legitimate way to start, not just a toy: about $3 a month, secure by default, and fast enough for 1,000 simulated students on a laptop CPU from 2016. The trade-off is that uptime and backups are entirely on you. Start here, measure, and move the day an outage would cost more than the server.
Would I do it again? Yes: 12 days at home showed me my real bottleneck (password hashing, not the database) before I paid for anything. The first thing I would change is disabling sleep on day one; it caused all of my downtime.
Some links are affiliate links; see the disclosure.




