Render is a modern cloud hosting platform that deploys Node.js applications from GitHub with minimal configuration. The free tier runs your Express API on a shared machine and automatically redeploys when you push to your chosen branch. The main trade-off on the free tier is cold starts โ if no request arrives for 15 minutes, the service spins down and the next request takes 30โ60 seconds to wake it up. For a learning project this is acceptable. In this lesson you will connect your GitHub repository to Render, configure the environment variables, verify the deployment, and understand how to diagnose issues from the logs.
Preparing Express for Deployment
// server/package.json โ ensure start script is defined
{
"scripts": {
"start": "node index.js", // Render uses this to start the server
"dev": "nodemon index.js"
},
"engines": {
"node": ">=20.0.0" // specify Node.js version
}
}
// server/index.js โ use process.env.PORT (Render assigns it automatically)
const PORT = process.env.PORT || 5000;
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
PORT environment variable automatically โ do not hardcode a port number. Always use process.env.PORT || 5000 so the app works both on Render (where PORT is set by the platform) and locally (where it falls back to 5000). If your app listens on a hardcoded port instead of process.env.PORT, Render cannot route traffic to it and the deployment will fail health checks.GET /api/health that returns { status: 'ok' } is enough. If the health check endpoint fails, Render marks the service as unhealthy and rolls back to the previous deployment โ which is exactly the safety net you want.Deploying to Render โ Step by Step
1. Push server/ to GitHub (ensure package.json, index.js, src/ are committed)
2. Go to https://render.com โ New โ Web Service
3. Connect GitHub repository
โ Select your mern-blog repository
โ Root Directory: server (if monorepo) or leave blank
4. Configure the service:
Name: mernblog-api
Runtime: Node
Build Command: npm install
Start Command: npm start
Instance Type: Free
5. Add environment variables (Environment tab):
NODE_ENV = production
PORT = (leave blank โ Render sets this automatically)
MONGODB_URI = mongodb+srv://mernblog-prod:...@cluster.mongodb.net/mernblog
JWT_SECRET = [64-char random hex]
JWT_EXPIRES_IN = 7d
CLIENT_URL = https://mernblog.netlify.app
SERVER_URL = https://mernblog-api.onrender.com
CLOUDINARY_CLOUD_NAME = ...
CLOUDINARY_API_KEY = ...
CLOUDINARY_API_SECRET = ...
RESEND_API_KEY = re_...
6. Click "Create Web Service"
โ Render builds the Docker container and deploys
โ Watch the deploy logs โ look for "Server running on port ..."
7. Test the health check:
curl https://mernblog-api.onrender.com/api/health
โ { "status": "ok", "env": "production" }
Adding the Health Check and Compression
// server/index.js โ production additions
const compression = require('compression');
const helmet = require('helmet');
npm install compression helmet
// โโ Security headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' }, // allow Cloudinary images
}));
// โโ Gzip compression โ reduces response size 60โ80% โโโโโโโโโโโโโโโโโโโโโโโโโโ
app.use(compression());
// โโ Health check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
env: process.env.NODE_ENV,
uptime: Math.round(process.uptime()),
timestamp: new Date().toISOString(),
});
});
Reading Render Logs
Render Dashboard โ Your Service โ Logs tab
Common log messages and their meaning:
โ "Server running on port 10000"
โ App started successfully โ deployment succeeded
โ "MongoServerSelectionError: connection timed out"
โ MONGODB_URI wrong, or Atlas IP allowlist missing Render's IP
โ Fix: verify MONGODB_URI env var, add Render IP to Atlas allowlist
โ "Error: ENOENT: no such file or directory, open '.env'"
โ .env file not committed (correct! .env should never be committed)
โ Fix: set all values in Render's Environment tab instead
โ "SyntaxError: Unexpected token ..."
โ Syntax error in your JavaScript โ check the specific file/line
โ Port already in use
โ You hardcoded a port โ use process.env.PORT instead
Automatic Deploys from GitHub
Render auto-deploys when you push to the connected branch (usually 'main').
Workflow:
1. Make changes locally
2. git add . && git commit -m "feat: add new endpoint"
3. git push origin main
4. Render detects the push โ starts a new build
5. Build runs: npm install
6. If build succeeds: new version deployed
7. If build fails: previous version stays live
Manual deploy: Render Dashboard โ Service โ Manual Deploy โ Deploy latest commit
Common Mistakes
Mistake 1 โ Hardcoding the PORT
โ Wrong โ app listens on a specific port:
server.listen(5000); // Render assigns a different port โ traffic never arrives!
โ Correct โ use the PORT environment variable:
server.listen(process.env.PORT || 5000); // โ
Mistake 2 โ Committing node_modules
โ Wrong โ node_modules committed to the repository:
# .gitignore missing node_modules entry
# โ 300MB of modules pushed to GitHub and pulled by Render every deploy
โ
Correct โ add node_modules to .gitignore. Render runs npm install during build.
Mistake 3 โ Missing environment variables on Render
โ Wrong โ env vars not set on Render, only in local .env file:
JWT_SECRET=undefined โ jwt.sign throws "secretOrPrivateKey must have a value"
โ Correct โ set every required environment variable in Render’s Environment tab before deploying.
Quick Reference
| Task | Detail |
|---|---|
| Build command | npm install |
| Start command | npm start |
| Listen on PORT | process.env.PORT || 5000 |
| Health check URL | https://your-service.onrender.com/api/health |
| View logs | Render Dashboard โ Service โ Logs |
| Trigger redeploy | Push to connected GitHub branch |
| Manual redeploy | Dashboard โ Manual Deploy |