How I Tracked Down the One‑Minute Timeout “Connection Refused” Bug When Deploying a NestJS API to a Cheap VPS with Shared Node Version Control 🚑
Hook: I was ready to launch my MVP, but after one minute the API crashed with “Connection Refused”. The whole thing happened on a $5/month VPS that shared a Node version with dozens of other users. What went wrong? Spoiler: It was a silent timeout caused by a mismatched Node version and a stray firewall rule. This post shows you exactly how I found the culprit in 30 minutes and how you can avoid the same nightmare.
Why This Matters
If you’re building a SaaS, a side‑hustle, or a freelance automation tool, you often choose the cheapest VPS to keep costs low. Those servers usually run nvm or shared /usr/bin/node binaries. A tiny version mismatch can turn a perfectly fine NestJS service into a “connection refused” ghost that disappears after 60 seconds. The cost? Hours of debugging, angry customers, and a missed launch window.
Step‑by‑Step Tutorial
-
Reproduce the Error Locally
Run the same Docker image you plan to push to the VPS. If the API works locally but not on the server, the problem is environmental.
Tip: Use
docker-compose upwith the sameenvfile you’ll use on the VPS. -
Check the Node Version on the VPS
ssh user@cheap-vps node -v # <-- what you getIf the output is
v12.xbut your NestJS project requiresv18, you’ve already found the first red flag. -
Inspect the Global
npmPackagesnpm list -g --depth=0Shared servers often have a globally installed
pm2ornestjs-clithat was compiled against an older Node version, causing silent crashes. -
Verify Firewall & SELinux Settings
Even if the port (e.g.,
3000) is open, a per‑process timeout can be enforced byiptables“connection‑track” rules.sudo iptables -L -n -v | grep 3000 -
Enable Detailed Logging in NestJS
Add a logger middleware that writes to
stdoutand a file. This catches the moment the process dies.import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as fs from 'fs'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn', 'log', 'debug', 'verbose'], }); const logStream = fs.createWriteStream('app.log', { flags: 'a' }); app.useLogger(app.getLogger()); process.stdout.write = process.stderr.write = logStream.write.bind(logStream); await app.listen(3000); } bootstrap(); -
Deploy with the Correct Node Version
Install
nvm(if allowed) and force the exact version yourpackage.jsonspecifies.curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash source ~/.bashrc nvm install 18.20.0 nvm use 18.20.0 node -v # should print v18.20.0 -
Restart the Service with
pm2and Watch the Logspm2 start dist/main.js --name nest-api pm2 logs nest-api --lines 100
Warning: Do not skip the nvm install step on a shared VPS. The default node binary is often an ancient LTS that lacks support for newer ES modules used by NestJS.
Real‑World Use Case
My client needed a quick URL‑shortener API. We built it with NestJS, TypeORM, and a PostgreSQL instance on the same VPS. The app ran perfectly on my Mac, but the first request from the client’s frontend hit a “Connection Refused” after exactly 60 seconds. After following the steps above, we discovered:
- The VPS was running
node v12.22.1(the default for the provider). - Our
package.jsondeclared"engines": {"node": ">=18"}, sonpm installsucceeded but the runtime threw a silentERR_REQUIRE_ESMafter the first async call. - Once we switched to
nvmand installed Node 18, the API responded in 45 ms instead of timing out.
Results / Outcome
By fixing the Node version and clearing the stray firewall rule, the API stayed alive indefinitely. Here’s a quick snapshot of the performance before and after:
# Before fix (timeout after 60s)
GET /shorten → connection refused (ERR_CONNECTION_REFUSED)
# After fix
GET /shorten → 200 OK, 41ms response time
Server uptime: 7 days 23 hrs (no restarts)
Result: Zero downtime, 10× faster response, and the client could finally launch their marketing campaign on schedule.
Bonus Tips
- Lock Node version in
.nvmrc:echo "18.20.0" > .nvmrcand addnvm useto your deployment script. - Use a health‑check endpoint: Add
/healthzthat returns200only when DB and external services are reachable. - Enable
systemdwatchdog: Prevent silent kills by configuringWatchdogSec=30in the service file. - Monitor with
pm2 status: Set up an email alert forstatus !== online. - Auto‑restart on version mismatch: Add a pre‑start script that checks
node -vagainstpackage.jsonand aborts if they differ.
Monetize This Knowledge
If you’re tired of chasing “one‑minute timeout” ghosts, I’ve compiled a 30‑page cheat sheet with exact VPS setup commands for the top 5 cheapest providers. Grab it now for $9 and stop losing hours to environment headaches.
© 2026 Your Blog Name – All rights reserved.
No comments:
Post a Comment