This article is published in English.
A Practical Framework for Taking Node.js Apps to Production
Walk through a complete production checklist for Node.js apps, covering servers, secrets, process management, HTTPS, CI/CD, logging, and backups.
Running a Node.js app on your own machine is the easy part.
npm run dev
Your API responds. Your database connects. Everything seems to work fine.
Then someone asks:
"Okay, how do we deploy this to production?"
That's when it becomes clear that building the API was only half the job.
Moving to production brings a whole new set of questions:
- Where will the app actually run?
- What keeps it running if it crashes?
- How does incoming traffic reach your Node.js process?
- Where should environment variables be stored?
- How do you set up HTTPS?
- How do you push out new versions?
- How do you catch errors when they happen?
- What happens if the server itself reboots?
Here's a practical framework for thinking through a Node.js production setup.
1. Understand the Production Architecture
A basic production setup typically looks like this:
Internet
│
▼
┌─────────┐
│ Nginx │
│ :80/443│
└────┬────┘
│
▼
┌─────────────┐
│ Node.js │
│ Application │
└──────┬──────┘
│
┌────────┼────────┐
▼ ▼ ▼
PostgreSQL Redis External APIs
The key principle is that end users generally should not connect directly to something like:
localhost:3000
Instead, Nginx sits in front, accepting public requests and passing them along to your Node.js app.
2. Prepare the Server
You can provision a VPS or a cloud virtual machine from a provider like AWS, GCP, Azure, or DigitalOcean.
Before anything else, you need to set up the machine itself.
On Ubuntu, that usually starts with:
sudo apt update
sudo apt upgrade -y
From there, install whatever tooling your app depends on.
For Node.js itself, it's generally a good idea to install it through a version manager like nvm, so you can control exactly which version runs.
Confirm the installed versions with:
node -v
npm -v
Whatever Node version runs in production should match what you test locally.
It seems like a minor detail, but mismatched versions can cause frustrating and hard-to-trace bugs once deployed.
3. Don't Put Secrets Inside Your Code
This mistake is easy to make and easy to avoid.
Avoid hardcoding credentials like this:
const DATABASE_URL =
"postgresql://user:password@database.com/mydb";
And never commit secrets into your Git history.
Instead, define them as environment variables:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://...
REDIS_URL=redis://...
JWT_SECRET=...
Then read them in your code using:
process.env.DATABASE_URL
Make sure your .env files are excluded from version control:
.env
.env.production
A single exposed database password can cause far more damage than any deployment mistake.
4. Build the Application
Before you launch the app, install only production dependencies, and run a build step if your framework needs one.
A TypeScript project, for instance, might run:
npm ci
npm run build
And then start the compiled output with:
npm start
The exact commands will vary depending on your stack.
What matters is this principle:
Production traffic should hit your production build, never your dev server.
In other words, don't accidentally leave something like:
npm run dev
running as your live process.
5. What Happens If Node.js Crashes?
Suppose you launch your app directly like this:
node dist/server.js
At some point, something goes wrong and the process dies unexpectedly.
Your API is now offline, with nothing bringing it back.
This is exactly the problem a process manager solves.
PM2 is a widely used choice for this.
Install it globally:
npm install -g pm2
Then launch your app under PM2's supervision:
pm2 start dist/server.js --name my-api
Check its status:
pm2 status
View logs:
pm2 logs my-api
Or restart it on demand:
pm2 restart my-api
The point isn't just ease of use. It's that your process is now actively supervised, instead of running in a terminal window and hoping nothing kills it.
6. Make the Application Start After a Server Restart
Servers do get rebooted, whether intentionally or not.
For instance:
Server reboot
↓
Operating system starts
↓
Node.js application?
You don't want to have to SSH in manually every single time that happens.
PM2 can generate a startup script for your system:
pm2 startup
Then persist your currently running process list:
pm2 save
With that in place, your application can automatically come back online after a reboot.
7. Put Nginx in Front of Node.js
Say your Node.js server is bound to:
localhost:3000
But your users are hitting:
https://api.example.com
Nginx can bridge that gap by acting as a reverse proxy.
A stripped-down config might look like:
server {
listen 80;server_name api.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
With that, the request path becomes:
User
↓
https://api.example.com
↓
Nginx :80
↓
Node.js :3000
This way, the port your Node.js app listens on never has to be exposed directly to the public internet.
8. Add HTTPS
Running your production API on plain
http://
isn't good enough. You need it served over
https://
A widely used approach is combining Let's Encrypt with Certbot.
Start by installing Certbot:
sudo apt install certbot python3-certbot-nginx
Then request and apply a certificate for your domain:
sudo certbot --nginx -d api.example.com
Certbot takes care of provisioning the certificate and wiring up HTTPS on your behalf.
Once that's done, your request flow looks like this:
Client
↓
HTTPS
↓
Nginx
↓
Node.js
↓
Database / Redis
9. Don't Forget Your Firewall
Not every port on your server needs to be reachable from the outside world.
Typically, you'd want the public to reach:
80 → HTTP
443 → HTTPS
along with SSH access on:
22
Ports used by PostgreSQL or Redis, on the other hand, usually have no business being open to the internet unless you have a specific reason and solid access controls in place.
The exact rules will vary depending on your setup, but the guiding rule stays the same:
Only expose what genuinely needs to be exposed.
10. Manual Deployment Works, Until It Doesn't
Early on, a deploy might just be:
git pull
npm install
npm run build
pm2 restart my-api
That's perfectly fine for a small project.
But sooner or later you'll notice you're going through the same sequence again and again:
Developer pushes code
↓
SSH into server
↓
git pull
↓
install dependencies
↓
build
↓
restart
At that point, it's worth automating.
11. CI/CD Changes the Workflow
Using something like GitHub Actions, the process can shift to:
Developer
↓
git push
↓
GitHub
↓
CI/CD Pipeline
↓
Build + Test
↓
Deploy
↓
Production
A minimal workflow definition might look like:
name: Deploy
on:
push:
branches:
- mainjobs:
deploy:
runs-on: ubuntu-latest steps:
- uses: actions/checkout@v4 - name: Install dependencies
run: npm ci - name: Build
run: npm run build - name: Test
run: npm test
The specific deployment steps will depend on your own infrastructure.
What matters more is the underlying principle:
Don't automate a deployment process you don't yet understand.
Learn how to do it by hand first.
Only then turn the repetitive steps into automation.
12. Logging Is Not Optional
An app can appear to be running fine even while real users are hitting errors.
Logs are how you find out.
At the very least, you want answers to:
When did the error happen?
Which endpoint failed?
What status code was returned?
What was the error?
How long did the request take?
A basic example:
console.error({
message: error.message,
endpoint: req.originalUrl,
method: req.method,
timestamp: new Date().toISOString()
});
For anything running at real scale, structured logs paired with centralized log aggregation will get you much further than scattering console.log() calls throughout your code.
13. Monitor More Than Errors
The absence of exceptions doesn't mean your system is healthy.
You also want visibility into things like:
CPU
Memory
Disk
Request latency
Error rate
Database performance
Redis health
Traffic
For instance:
Requests → 1,500/min
Average latency → 180ms
Error rate → 0.4%
CPU → 42%
Memory → 61%
Metrics like these give you a far more complete picture of how your system is actually performing.
14. Backups Matter More Than Deployment
Here's a situation most developers would rather not think about:
Production database
↓
Something goes wrong
↓
Data disappears
You can always redeploy your application code.
Your database, however, may hold data that simply can't be regenerated once it's gone.
That's why backups aren't optional either.
You need a backup plan for anything important in production, and just as importantly, you need to actually know how to restore from those backups.
A backup you've never tried restoring isn't one you should trust.
15. Zero-Downtime Deployments Are a Separate Problem
At some point, briefly taking your app offline to deploy stops being acceptable.
Consider this scenario:
Old version running
↓
New version deployed
↓
Traffic gradually moves
↓
Old version removed
Depending on how your infrastructure is set up, you might reach for:
- Running several copies of your Node.js process side by side
- PM2's built-in cluster mode
- A load balancer distributing traffic across instances
- Deploying updates gradually, node by node, instead of all at once
- Switching traffic between an old and a new environment (blue-green style)
- Packaging the app in containers
- Orchestrating everything with Kubernetes
Still, having a Node.js API doesn't automatically mean you need Kubernetes.
Keep it simple at first, and grow the architecture only once your actual requirements demand it.
16. My Production Checklist
Before considering a Node.js app ready for production, here's what's worth going through:
[ ] Production environment configured
[ ] Secrets stored securely
[ ] Database connection configured
[ ] Redis configured if required
[ ] Production build tested
[ ] Process manager configured
[ ] Application restart tested
[ ] Nginx configured
[ ] HTTPS configured
[ ] Firewall configured
[ ] Logs available
[ ] Error monitoring configured
[ ] Database backups configured
[ ] Backup restoration tested
[ ] Deployment process documented
[ ] CI/CD configured if needed
[ ] Health check endpoint available
17. The Architecture I Keep in Mind
A basic production setup for a Node.js system tends to converge on something like this:
┌─────────────┐
│ Internet │
└──────┬──────┘
│
▼
┌─────────────┐
│ Nginx │
│ SSL / Proxy│
└──────┬──────┘
│
┌─────────┴─────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Node.js │ │ Node.js │
│ Instance 1 │ │ Instance 2 │
└─────┬──────┘ └─────┬──────┘
│ │
└─────────┬─────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
PostgreSQL Redis External APIs
And wrapped around that core, you'll typically want:
Monitoring
Logging
Backups
CI/CD
Security
Final Thought
Shipping a Node.js app to production is never just:
npm start
Real production work means planning for everything that unfolds after the process boots.
What's your plan if it goes down unexpectedly?
How does the system behave once the server reboots?
What's the response when incoming traffic suddenly climbs?
What happens the moment the database becomes unreachable?
How do you recover once a faulty release ships?
What's the protocol if a credential ends up exposed by mistake?
That's the gap between:
"It works on my machine."
and
"It runs reliably in production."
You don't need a huge infrastructure stack on day one.
Start small.
Understand each piece you add.
Automate whatever repeats.
Watch the metrics that actually matter.
Only introduce complexity once the system genuinely calls for it.
Deployment isn't the finish line of development. It's where operating your software actually begins.