Node.js powers over 6.3 million websites worldwide and remains the go-to runtime for building scalable APIs, real-time applications, and microservices in 2026. With Node.js v22 LTS (codename “Iron”) now the current long-term support release, and Ubuntu 24.04 LTS (Noble Numbat) as the dominant Linux server OS, deploying Node.js has never been more streamlined — but only when you follow the right steps.
Skipping a process manager, running your app without a reverse proxy, or exposing your Node.js port directly to the internet are mistakes that can bring a production server down within hours. This guide walks you through every step correctly — from server setup to CI/CD automation — so your deployment is secure, stable, and ready to scale.
By the end of this guide, you will have a fully working Node.js application running on a Linux server with PM2 process management, Nginx reverse proxy, Let’s Encrypt SSL, UFW firewall protection, and an automated GitHub Actions deployment pipeline.
Before You Begin — Prerequisites
Make sure you have the following ready before starting:
• A Linux server running Ubuntu 24.04 LTS (recommended) or 22.04 LTS minimum
• SSH access to the server with sudo privileges
• A domain name pointed to your server’s public IP address (needed for SSL)
• Your Node.js application code — either locally or hosted on a GitHub repository
• Basic familiarity with the Linux terminal and command line
What you will build by the end of this guide:
A production-grade Node.js deployment with PM2 cluster mode, Nginx reverse proxy, HTTPS via Let’s Encrypt, UFW firewall, Fail2ban brute-force protection, and GitHub Actions CI/CD.
1. Prepare Your Node.js Application for Production
Before touching your server, your application code must be production-ready. Here is what to check and set up.
Getting Your Code Ready
Go through your codebase and:
• Remove unused npm packages from package.json
• Optimise database queries and add indexes where needed
• Implement proper error handling — unhandled exceptions crash PM2 workers
• Make sure all sensitive values (API keys, database URLs, secrets) are in environment variables, never hardcoded
Setting Up Environment Variables with dotenv
Environment variables keep sensitive data out of your code and out of version control.
Step 1 — Install the dotenv package:
npm install dotenv
Step 2 — Create a .env file in your project root:
PORT=3000
DB_URL=mongodb://localhost:27017/mydb
SECRET_KEY=your_secret_key_here
NODE_ENV=production
Step 3 — Load it at the very top of your main file (server.js or app.js):
require(‘dotenv’).config();
const port = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;
Step 4 — Add .env to your .gitignore file so it never gets pushed to GitHub:
echo “.env” >> .gitignore
Configuring package.json Scripts
Make sure your package.json has the correct scripts:
“scripts”: {
“start”: “node server.js”,
“dev”: “nodemon server.js”,
“prod”: “NODE_ENV=production node server.js”,
“test”: “jest –coverage”
}
To install only production dependencies on the server (excludes devDependencies), use:
npm ci –omit=dev
Note: “npm install –production” is deprecated as of npm v9+. Always use “npm ci –omit=dev” on production servers.
Creating a PM2 Ecosystem File
Instead of starting your app with a simple command, create an ecosystem.config.js file in your project root. This is the correct PM2 configuration method in 2026:
module.exports = {
apps: [{
name: ‘myapp’,
script: ‘server.js’,
instances: ‘max’,
exec_mode: ‘cluster’,
env: {
NODE_ENV: ‘development’
},
env_production: {
NODE_ENV: ‘production’,
PORT: 3000
},
error_file: ‘./logs/err.log’,
out_file: ‘./logs/out.log’,
log_date_format: ‘YYYY-MM-DD HH:mm:ss’
}]
};
The “instances: max” setting tells PM2 to use all available CPU cores, and “exec_mode: cluster” enables Node.js cluster mode — which means your app handles far more traffic than a single-process setup.
2. Setting Up a Linux Server
Choosing a Cloud Provider
Selecting the right cloud provider affects your app’s speed, cost, and reliability. Here are the top options in 2026:
1. CloudMinister
CloudMinister is an affordable cloud hosting provider offering stable, Node.js-optimised Linux VPS servers for developers and businesses in India and globally. It provides VPS hosting, dedicated servers, and cloud infrastructure with a focus on performance, security, and 24/7 managed support. CloudMinister is an excellent choice for startups and enterprises who want a cost-effective, fully supported environment for running Node.js applications without the complexity of hyperscaler platforms.
2. Amazon Web Services (AWS)
AWS EC2 is the world’s leading cloud compute platform, offering hundreds of instance types, global availability zones, auto-scaling groups, and deep integration with other AWS services like RDS, S3, and CloudFront. It suits any project size, from a small REST API to an enterprise microservices architecture.
3. DigitalOcean
DigitalOcean Droplets are developer-friendly virtual machines that balance simplicity and performance. With SSD storage, one-click deployments, a clean dashboard, and transparent flat-rate pricing, DigitalOcean is a top choice for individual developers, startups, and small teams deploying Node.js applications.
4. Akamai Cloud (formerly Linode)
Akamai Cloud — acquired and rebranded from Linode in 2023 — offers high-performance compute instances with transparent, predictable pricing and no surprise bills. In 2026, it provides Dedicated CPU instances, GPU servers, and managed Kubernetes. It is well-suited for developers who want bare-metal performance without cloud vendor lock-in.
Recommended OS: Ubuntu 24.04 LTS
Select Ubuntu 24.04 LTS (Noble Numbat) as your server operating system. It is the current LTS release, supported until April 2029, and the recommended choice for all Node.js production deployments in 2026.
Do not use CentOS — it reached End of Life in December 2021 and no longer receives security updates. If you need a RHEL-based system, use Rocky Linux 9 or AlmaLinux 9 instead.
Connecting to Your Server via SSH
Once your instance is running, connect using SSH:
ssh your-username@your-server-ip
Always use SSH key authentication instead of password authentication. To generate an SSH key pair on your local machine (if you have not already done so):
ssh-keygen -t ed25519 -C “[email protected]”
Then copy your public key to the server:
ssh-copy-id your-username@your-server-ip
Hardening SSH Access (Do This Immediately After First Login)
Open the SSH configuration file:
sudo nano /etc/ssh/sshd_config
Set these values:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Save the file, then restart SSH:
sudo systemctl restart sshd
This ensures no one can brute-force their way into your server using a password.
3. Installing Node.js and Required Dependencies
Updating the System
Always update your package list before installing anything:
sudo apt update && sudo apt upgrade -y
Node.js Version Reference for 2026
Before installing, understand which version to use:
| Version | Status in 2026 | Recommendation |
| v22.x LTS (Iron) | Active LTS — supported until April 2027 | USE THIS |
| v20.x LTS (Iron) | Maintenance LTS — ends mid-2026 | Acceptable |
| v18.x (Hydrogen) | End of Life — April 2025 | Do NOT use |
| v16.x (Gallium) | End of Life — September 2023 | Do NOT use |
Installing Node.js via NVM (Recommended Method)
NVM (Node Version Manager) is the industry-standard way to install Node.js on a Linux server in 2026. It lets you install and switch between multiple Node.js versions without system-level conflicts.
Step 1 — Install NVM:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Step 2 — Reload your shell:
source ~/.bashrc
Step 3 — Install Node.js v22 LTS:
nvm install 22
nvm use 22
nvm alias default 22
Step 4 — Verify the installation:
node -v
npm -v
You should see v22.x.x and npm 10.x.x.
Alternative: Install Node.js via NodeSource (System-Wide)
If you need Node.js installed system-wide (for all users), use the official NodeSource binary:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash –
sudo apt-get install -y nodejs
node -v && npm -v
Installing PM2
PM2 is the process manager that keeps your Node.js app running in the background, restarts it on crashes, and manages logs.
If you installed Node.js with NVM, do NOT use sudo:
npm install -g pm2
pm2 –version
If you installed Node.js system-wide via NodeSource or apt, use sudo:
sudo npm install -g pm2
Installing Nginx
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
4. Deploying the Node.js Application
Setting Up the Application Directory
Create a dedicated directory for your application:
sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
Recommended production directory structure:
/var/www/myapp/
├── server.js
├── ecosystem.config.js
├── package.json
├── package-lock.json
├── .env
├── node_modules/
└── logs/
├── err.log
└── out.log
Transferring Your Application Files
Recommended method — Git (industry standard in 2026):
cd /var/www/myapp
git clone https://github.com/your-username/your-repo.git .
For private repositories, use a GitHub fine-grained personal access token:
git clone https://oauth2:[email protected]/your-username/your-repo.git .
Alternative method — SCP (for quick one-time transfers):
scp -r /local/path/to/app your-username@your-server-ip:/var/www/myapp
Installing Dependencies on the Server
cd /var/www/myapp
npm ci –omit=dev
Starting the App with PM2
Use the ecosystem.config.js file created in Section 1:
pm2 start ecosystem.config.js –env production
Or start directly without the ecosystem file:
pm2 start server.js –name “myapp”
Check that your app is running:
pm2 status
pm2 logs myapp
Making PM2 Start Automatically on Server Reboot
pm2 startup systemd
PM2 will output a command — copy it exactly and run it. Then save the process list:
pm2 save
From this point, your app will automatically restart whenever the server reboots.
5. Configuring a Reverse Proxy with Nginx
Why You Need a Reverse Proxy
A reverse proxy sits between the internet and your Node.js application. It is not optional in production — it is essential. Here is what Nginx does for you:
Enhanced Security: Your Node.js app never directly faces the internet. Nginx acts as the shield, hiding your backend from direct attacks such as DDoS, request flooding, and direct port scanning.
SSL Termination: Nginx handles HTTPS encryption and decryption so your Node.js app does not have to — improving performance and simplifying your code.
Load Balancing: Nginx can distribute traffic across multiple Node.js processes or servers, ensuring no single instance is overwhelmed.
Gzip Compression: Nginx compresses responses before sending them to the browser, reducing bandwidth usage and improving page load speed significantly.
Static File Serving: Nginx serves CSS, JS, image files, and other static assets directly — much faster than routing them through Node.js.
Access Control: You can set rate limits, IP whitelists, and authentication rules at the Nginx level, before requests even reach your app.
Installing and Configuring Nginx
Create a new Nginx configuration file for your application:
sudo nano /etc/nginx/sites-available/myapp
Paste the following complete production-ready configuration:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Security headers
add_header X-Frame-Options “SAMEORIGIN” always;
add_header X-Content-Type-Options “nosniff” always;
add_header X-XSS-Protection “1; mode=block” always;
add_header Referrer-Policy “no-referrer-when-downgrade” always;
# Gzip compression
gzip on;
gzip_types text/plain application/json application/javascript text/css;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection ‘upgrade’;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
proxy_connect_timeout 60s;
}
# Serve static files directly via Nginx (much faster)
location /static/ {
alias /var/www/myapp/public/;
expires 30d;
add_header Cache-Control “public, immutable”;
}
}
Replace yourdomain.com with your actual domain name.
Enable the configuration by creating a symbolic link:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
Always test the configuration before reloading — a single typo will take your site offline:
sudo nginx -t
If you see “syntax is ok” and “test is successful”, reload Nginx:
sudo systemctl reload nginx
Use “reload” instead of “restart” — reload applies changes with zero downtime. Restart briefly stops and starts Nginx, causing a momentary outage.
6. Securing Your Deployment
Setting Up a Firewall with UFW
UFW (Uncomplicated Firewall) controls which ports are open on your server. Correct setup is critical.
Set the default policies first — deny all incoming traffic, allow all outgoing:
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow SSH access (do this before enabling UFW, or you will lock yourself out):
sudo ufw allow OpenSSH
Allow HTTP and HTTPS through Nginx:
sudo ufw allow ‘Nginx Full’
Block direct access to your Node.js port — users must only reach your app through Nginx, never directly:
sudo ufw deny 3000/tcp
Enable UFW and check the rules:
sudo ufw enable
sudo ufw status verbose
Securing with SSL — Let’s Encrypt and Certbot
Install Certbot using the official snap method (recommended in 2026 — the apt version is outdated):
sudo snap install –classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Obtain your SSL certificate for both your domain and www subdomain:
sudo certbot –nginx -d yourdomain.com -d www.yourdomain.com
Certbot automatically configures Nginx for HTTPS and sets up a systemd timer for auto-renewal. Verify the renewal timer is active:
sudo systemctl status snap.certbot.renew.timer
Test that auto-renewal works correctly:
sudo certbot renew –dry-run
After SSL is installed, check your security grade at ssllabs.com/ssltest — you should achieve an A or A+ rating.
Blocking Brute-Force Attacks with Fail2ban
Fail2ban monitors your server logs and automatically bans IP addresses that repeatedly fail SSH login attempts. It is essential for any internet-facing server.
Install Fail2ban:
sudo apt install fail2ban -y
Create a local configuration file:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Find the [sshd] section and set these values:
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 3600
findtime = 600
This bans any IP that fails SSH login 3 times within 10 minutes, for 1 hour.
Enable and start Fail2ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status sshd
Securing Your Node.js Application Code
Install Helmet.js — a security middleware that automatically sets 11 HTTP security headers in your Express.js app:
npm install helmet
npm install express-rate-limit
Add these to the top of your server.js or app.js, before any routes:
const express = require(‘express’);
const helmet = require(‘helmet’);
const rateLimit = require(‘express-rate-limit’);
const app = express();
// Apply security headers
app.use(helmet());
// Tell Express to trust the Nginx proxy
app.set(‘trust proxy’, 1);
// Rate limiting — max 100 requests per 15 minutes per IP
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(‘/api/’, limiter);
Security Best Practices Checklist
• Store all secrets and credentials in .env — never hardcode them
• Add .env to .gitignore — never commit it to version control
• Run your app as a non-root user (PM2 handles this by default)
• Keep system packages updated: sudo apt update && sudo apt upgrade -y
• Review PM2 logs regularly for errors and unusual activity
7. Automating Deployment with CI/CD
Introduction to CI/CD Pipelines
A CI/CD pipeline (Continuous Integration / Continuous Deployment) automates the process of testing and deploying your code every time you push a change. Instead of manually SSHing into your server, pulling code, and restarting PM2, a pipeline does it all for you in under 60 seconds — and only deploys if your tests pass.
How CI/CD Works
Continuous Integration (CI): Every time a developer pushes code to the repository, the pipeline automatically installs dependencies and runs the test suite. If any test fails, deployment is blocked. Bugs are caught before they reach production.
Continuous Deployment (CD): If all tests pass, the pipeline SSHs into your production server, pulls the latest code, reinstalls dependencies, and reloads the app — with zero manual steps and zero downtime.
Why It Matters
Speed: Deployment that took 15 minutes manually now takes under 60 seconds automatically.
Quality: Automated tests catch bugs before production. No more “it worked on my machine.”
Reliability: Every deployment follows the exact same steps — no human error, no forgotten commands.
Confidence: You can push code 10 times a day without fear.
Using GitHub Actions
In your project repository, create the following file:
Path: .github/workflows/deploy.yml
name: Deploy Node.js to Linux Server
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v4
– name: Setup Node.js v22
uses: actions/setup-node@v4
with:
node-version: ’22’
cache: ‘npm’
– name: Install dependencies
run: npm ci
– name: Run tests
run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == ‘refs/heads/main’
steps:
– name: Deploy via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci –omit=dev
pm2 reload ecosystem.config.js –env production
pm2 save
Setting Up GitHub Secrets
Your workflow references three secrets. You must add them to GitHub before the pipeline will work.
Go to your GitHub repository → Settings → Secrets and Variables → Actions → New Repository Secret
Add these three secrets:
Secret name: SERVER_HOST
Value: Your server’s IP address (example: 203.0.113.42)
Secret name: SERVER_USER
Value: Your Linux username (example: ubuntu or deploy)
Secret name: SSH_PRIVATE_KEY
Value: The full contents of your private key file. To get it, run on your local machine:
cat ~/.ssh/id_ed25519
Copy the entire output (including the —–BEGIN and —–END lines) and paste it as the secret value.
Once these secrets are set, every push to your main branch will automatically test and deploy your application.
8. Monitoring and Maintenance
PM2 has built-in monitoring tools that give you real-time insight into your application’s health:
| Command | Description |
pm2 status |
Overview of all running apps |
pm2 monit |
Real-time CPU and memory dashboard |
pm2 logs myapp |
Live log stream |
pm2 logs myapp --lines 200 |
View last 200 lines of logs |
pm2 describe myapp |
Full process information |
pm2 flush |
Clear all log files |
Setting Up Log Rotation
Without log rotation, PM2 log files will grow indefinitely and eventually fill your server’s disk, causing a crash. Install PM2’s official log rotation module:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
This keeps a maximum of 7 rotated log files, each up to 10MB, compressed.
Setting Up Uptime Monitoring and Alerts
Use these free tools to get alerted if your site goes down:
BetterStack (betterstack.com) — free tier with 3-minute check intervals, email and SMS alerts, and a status page. This is the most recommended uptime monitor in 2026.
UptimeRobot (uptimerobot.com) — free with 5-minute check intervals and email alerts. Good for personal projects.
For application error tracking, use Sentry (sentry.io) — it captures unhandled exceptions in your Node.js app and sends alerts with a full stack trace and context.
Useful Server Health Commands
| Command | Description |
htop |
CPU and memory usage |
df -h |
Disk space usage |
sudo ss -tlnp |
Check which ports are open |
sudo tail -f /var/log/nginx/error.log |
Check Nginx error logs |
journalctl --since "1 hour ago" |
Check system logs for the last hour |
Scaling Your Application
Vertical Scaling: Upgrade your server — add more CPU cores, RAM, or disk space through your cloud provider’s control panel. PM2 cluster mode will automatically use the new cores.
Horizontal Scaling: Run multiple servers behind a load balancer. Use Nginx upstream blocks to distribute traffic across server instances. This approach allows your application to serve millions of requests while maintaining zero downtime during deployments.
Zero-Downtime Reloads with PM2
When deploying new code, always use “reload” instead of “restart”:
pm2 reload ecosystem.config.js –env production
PM2 reload restarts each worker process one at a time, so traffic is never interrupted. PM2 restart stops all processes simultaneously, causing a brief outage.
9. Troubleshooting Common Issues
Even with a correct setup, you will occasionally encounter issues. Here are the most common problems and their exact fixes.
502 Bad Gateway from Nginx
This means Nginx is running but your Node.js app is not, or is not listening on the expected port.
| Command | Description |
pm2 status |
Check if the app is running |
pm2 logs myapp |
Look for startup errors |
pm2 restart myapp |
Restart if it shows “stopped” or “errored” |
Also check your Nginx config — make sure “proxy_pass http://localhost:3000” matches the PORT in your .env file.
Port 3000 Already in Use
If you see “Error: listen EADDRINUSE :::3000”, another process is using your port.
| Command | Description |
sudo lsof -i :3000 |
Find what is using the port |
kill -9 [PID] |
Kill that process (replace [PID] with the number shown) |
App Crashes on Reboot
Your PM2 process list was not saved, or the startup hook was not installed.
| Scenario / Step | Command | Description |
| Startup Setup | pm2 startup systemd |
Generates a systemd startup command |
| Startup Setup | (Copy and run the output command) | Configures PM2 to launch on system boot |
| Startup Setup | pm2 save |
Saves the current process list for automatic restoration |
| Package-Lock Sync Fix | rm -rf node_modules package-lock.json |
Deletes existing dependencies and the out-of-sync lockfile |
| Package-Lock Sync Fix | npm install |
Performs a clean install and regenerates the lockfile |
| Package-Lock Sync Fix | git add package-lock.json |
Stages the newly generated lockfile |
| Package-Lock Sync Fix | git commit -m "Regenerate package-lock.json" |
Commits the changes to your repository |
Permission Denied on npm Global Install (EACCES)
This happens when you use “sudo npm install -g” with NVM.
| Context / Action | Command | Description |
| Fix Warning | Never use sudo with NVM-installed npm. |
Avoids permission conflicts and security risks. |
| Installation | npm install -g pm2 |
Correctly installs PM2 globally under your user profile. |
SSL Certificate Not Found After Certbot
| Scenario / Step | Command | Description |
| SSL Setup | sudo certbot --nginx -d yourdomain.com -d [www.yourdomain.com](https://www.yourdomain.com) |
Obtains and configures SSL certificate for the domains |
| SSL Setup | sudo nginx -t |
Tests the Nginx configuration for syntax errors |
| SSL Setup | sudo systemctl reload nginx |
Reloads Nginx to apply the new SSL configurations |
| Config Fix | sudo nginx -t |
Shows the exact file and line number with the error |
| Config Fix | (Edit /etc/nginx/sites-available/myapp) |
Manually correct the configuration mistake |
| Config Fix | sudo nginx -t |
Test again to ensure all syntax errors are resolved |
| Config Fix | sudo systemctl reload nginx |
Reloads Nginx safely to apply the working configuration |
Conclusion
Deploying a Node.js application on Linux in 2026 is a structured, repeatable process. When each layer is correctly configured, the result is a production system that is fast, resilient, and secure.
Here is a summary of what you have built through this guide:
- Node.js v22 LTS installed via NVM — the correct 2026 method
- Application running under PM2 in cluster mode — using all available CPU cores
- Nginx reverse proxy routing traffic with security headers and gzip compression
- HTTPS with a Let’s Encrypt SSL certificate and automatic renewal via systemd
- UFW firewall with port 3000 blocked from direct public access
- Fail2ban blocking SSH brute-force attempts automatically
- Helmet.js and express-rate-limit securing your Node.js application code
- GitHub Actions CI/CD pipeline — tests run first, then zero-downtime deployment on every push to main
- PM2 log rotation preventing disk fill in production
- Monitoring with PM2 monit, BetterStack uptime alerts, and Sentry error tracking
This is the exact same production stack used by professional DevOps teams worldwide. You now have the foundation to host any Node.js project — from a personal API to a high-traffic SaaS application.
Need a Linux VPS server that is already optimised for Node.js? CloudMinister offers Node.js-ready VPS plans with SSD storage, 99.99% uptime, and 24/7 expert support — so you can focus on your code, not your infrastructure. Explore our Node.js hosting plans and get your application live today.
Frequently Asked Questions – FAQs
1. What is the best way to install Node.js on Linux in 2026?
The best method is to use NVM (Node Version Manager). NVM lets you install and switch between multiple Node.js versions without root permissions and without breaking other tools on your system. Run:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
nvm alias default 22
This installs Node.js v22 LTS (Iron) — the current active LTS release, supported until April 2027. Alternatively, use the official NodeSource binary for system-wide installation.
2. How do I keep my Node.js application running after the terminal is closed or the server reboots?
Use PM2 — the industry-standard process manager for Node.js in production. After starting your app with PM2, run these two commands:
pm2 startup systemd
pm2 save
“pm2 startup systemd” generates a systemd service that starts PM2 automatically on boot. Copy and run the command it outputs. “pm2 save” saves your current process list so PM2 restores it on startup. Your app will now survive server reboots and terminal closures.
3. What is the easiest way to expose my Node.js app to the internet?
Use Nginx as a reverse proxy. Nginx accepts HTTP/HTTPS traffic on ports 80 and 443 and forwards it to your Node.js app on port 3000 (or whichever port you use). This is the production-standard setup in 2026:
server {
listen 80;
server_name yourdomain.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-Proto $scheme;
}
}
After saving the config: sudo nginx -t && sudo systemctl reload nginx
4. How do I automate deployments so I do not have to SSH in every time?
Use GitHub Actions CI/CD. Create a .github/workflows/deploy.yml file in your repository. Set three GitHub Secrets (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY) in your repository settings. Every time you push to the main branch, GitHub Actions will run your tests and, if they pass, automatically SSH into your server, pull the latest code, install dependencies, and reload PM2 — all without any manual steps.
5. How do I secure and optimise my Node.js deployment?
A complete production security setup in 2026 includes:
• UFW firewall: set default deny incoming, allow OpenSSH and Nginx Full, and block port 3000 from public access
• Let’s Encrypt SSL: use Certbot (snap method) to get free HTTPS certificates that auto-renew
• Fail2ban: automatically bans IPs after 3 failed SSH login attempts
• Helmet.js: sets 11 HTTP security headers in your Express app with one line of code
• express-rate-limit: limits API request rate per IP to prevent abuse
• .env file: keep all secrets out of your code and out of Git
• PM2 cluster mode: uses all CPU cores and enables zero-downtime reloads
6. Should I use Docker instead of PM2 for Node.js on Linux in 2026?
Both are valid choices, but they serve different purposes. PM2 is simpler, uses fewer server resources, and is ideal for a single Node.js application on a VPS. Docker is the better choice when you need consistent environments across development, staging, and production, or when you are running multiple services (databases, caches, workers) that need to be isolated. For most startups and individual projects in 2026, PM2 + Nginx on a VPS is the most cost-effective and straightforward approach.
7. Which Node.js version should I use in production in 2026?
Use Node.js v22 LTS (codename “Iron”). It is the current Active LTS release, supported until April 2027, and includes the latest performance improvements, security patches, and stable APIs. Avoid v18 (reached End of Life in April 2025) and v16 (reached End of Life in September 2023). Always install via NVM to make version upgrades easy in the future.
8. How do I deploy a Node.js app specifically on Ubuntu 24.04?
Ubuntu 24.04 LTS (Noble Numbat) is the recommended OS for Node.js in 2026. The entire process in this guide applies directly to Ubuntu 24.04. One important note: the default Node.js version installed by Ubuntu’s apt repository is v18, which is End of Life. Always install Node.js using NVM or the NodeSource v22 binary instead of running “sudo apt install nodejs” directly.
[ { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Node.js powers over 6.3 million websites worldwide and remains the go-to runtime for building scalable APIs, real-time applications, and microservices in 2026. With Node.js v22 LTS (codename \u201cIron\u201d) now the current long-term support release, and Ubuntu 24.04 LTS (Noble Numbat) as the dominant Linux server OS, deploying Node.js has never been more streamlined \u2014 but only when you follow the right steps.
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Skipping a process manager, running your app without a reverse proxy, or exposing your Node.js port directly to the internet are mistakes that can bring a production server down within hours. This guide walks you through every step correctly \u2014 from server setup to CI/CD automation \u2014 so your deployment is secure, stable, and ready to scale.
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “By the end of this guide, you will have a fully working Node.js application running on a Linux server with PM2 process management, Nginx reverse proxy, Let\u2019s Encrypt SSL, UFW firewall protection, and an automated GitHub Actions deployment pipeline.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 2 }, “innerBlocks”: [], “innerHTML”: “Before You Begin \u2014 Prerequisites
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Make sure you have the following ready before starting:
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “1. Prepare Your Node.js Application for Production
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Getting Your Code Ready
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Go through your codebase and:
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Setting Up Environment Variables with dotenv
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Environment variables keep sensitive data out of your code and out of version control.
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Step 1 \u2014 Install the dotenv package:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “npm install dotenv”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Step 2 \u2014 Create a .env file in your project root:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “PORT=3000\nDB_URL=mongodb://localhost:27017/mydb\nSECRET_KEY=your_secret_key_here\nNODE_ENV=production”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Step 3 \u2014 Load it at the very top of your main file (server.js or app.js):
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “require('dotenv').config();\nconst port = process.env.PORT || 3000;\nconst dbUrl = process.env.DB_URL;”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Step 4 \u2014 Add .env to your .gitignore file so it never gets pushed to GitHub:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “echo \".env\" >> .gitignore”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Configuring package.json Scripts
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Make sure your package.json has the correct scripts:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “\"scripts\": {\n \"start\": \"node server.js\",\n \"dev\": \"nodemon server.js\",\n \"prod\": \"NODE_ENV=production node server.js\",\n \"test\": \"jest --coverage\"\n}”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “To install only production dependencies on the server (excludes devDependencies), use:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “npm ci --omit=dev”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Note: \u201cnpm install –production\u201d is deprecated as of npm v9+. Always use \u201cnpm ci –omit=dev\u201d on production servers.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Creating a PM2 Ecosystem File
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Instead of starting your app with a simple command, create an ecosystem.config.js file in your project root:
module.exports = {\n apps: [{\n name: 'myapp',\n script: 'server.js',\n instances: 'max',\n exec_mode: 'cluster',\n env: {\n NODE_ENV: 'development'\n },\n env_production: {\n NODE_ENV: 'production',\n PORT: 3000\n },\n error_file: './logs/err.log',\n out_file: './logs/out.log',\n log_date_format: 'YYYY-MM-DD HH:mm:ss'\n }]\n};”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “2. Setting Up a Linux Server
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Choosing a Cloud Provider
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 4 }, “innerBlocks”: [], “innerHTML”: “1. CloudMinister
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “CloudMinister is an affordable cloud hosting provider offering stable, Node.js-optimised Linux VPS servers for developers and businesses in India and globally. It provides VPS hosting, dedicated servers, and cloud infrastructure with a focus on performance, security, and 24/7 managed support.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 4 }, “innerBlocks”: [], “innerHTML”: “2. Amazon Web Services (AWS)
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “AWS EC2 is the world\u2019s leading cloud compute platform, offering hundreds of instance types, global availability zones, auto-scaling groups, and deep integration with other AWS services like RDS, S3, and CloudFront.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 4 }, “innerBlocks”: [], “innerHTML”: “3. DigitalOcean
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “DigitalOcean Droplets are developer-friendly virtual machines that balance simplicity and performance. With SSD storage, one-click deployments, a clean dashboard, and transparent flat-rate pricing, DigitalOcean is a top choice for individual developers, startups, and small teams deploying Node.js applications.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 4 }, “innerBlocks”: [], “innerHTML”: “4. Akamai Cloud (formerly Linode)
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Akamai Cloud offers high-performance compute instances with transparent, predictable pricing. In 2026, it provides Dedicated CPU instances, GPU servers, and managed Kubernetes. It is well-suited for developers who want bare-metal performance without cloud vendor lock-in.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Recommended OS: Ubuntu 24.04 LTS
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Select Ubuntu 24.04 LTS (Noble Numbat) as your server operating system. It is the current LTS release, supported until April 2029, and the recommended choice for all Node.js production deployments in 2026.
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Do not use CentOS \u2014 it reached End of Life in December 2021. If you need a RHEL-based system, use Rocky Linux 9 or AlmaLinux 9 instead.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Connecting to Your Server via SSH
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “ssh your-username@your-server-ip”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Always use SSH key authentication instead of password authentication. To generate an SSH key pair on your local machine:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “ssh-keygen -t ed25519 -C \"[email protected]\"\nssh-copy-id your-username@your-server-ip”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Hardening SSH Access
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo nano /etc/ssh/sshd_config\n\nPermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\n\nsudo systemctl restart sshd”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “3. Installing Node.js and Required Dependencies
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Updating the System
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo apt update && sudo apt upgrade -y”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Node.js Version Reference for 2026
” }, { “blockName”: “core/table”, “attrs”: { “hasFixedLayout”: true }, “innerBlocks”: [], “innerHTML”: “| Version | Status in 2026 | Recommendation |
|---|---|---|
| v22.x LTS (Iron) | Active LTS \u2014 supported until April 2027 | USE THIS |
| v20.x LTS | Maintenance LTS \u2014 ends mid-2026 | Acceptable |
| v18.x (Hydrogen) | End of Life \u2014 April 2025 | Do NOT use |
| v16.x (Gallium) | End of Life \u2014 September 2023 | Do NOT use |
Installing Node.js via NVM (Recommended)
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash\nsource ~/.bashrc\nnvm install 22\nnvm use 22\nnvm alias default 22\nnode -v\nnpm -v”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Alternative: Install Node.js via NodeSource (System-Wide)
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -\nsudo apt-get install -y nodejs\nnode -v && npm -v”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Installing PM2
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “npm install -g pm2\npm2 --version”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Installing Nginx
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo apt install nginx -y\nsudo systemctl enable nginx\nsudo systemctl start nginx”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “4. Deploying the Node.js Application
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Setting Up the Application Directory
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo mkdir -p /var/www/myapp\nsudo chown $USER:$USER /var/www/myapp”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Transferring Your Application Files
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Recommended method \u2014 Git (industry standard in 2026):
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “cd /var/www/myapp\ngit clone https://github.com/your-username/your-repo.git .”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Installing Dependencies on the Server
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “cd /var/www/myapp\nnpm ci --omit=dev”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Starting the App with PM2
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 start ecosystem.config.js --env production\npm2 status\npm2 logs myapp”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Making PM2 Start Automatically on Server Reboot
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 startup systemd\npm2 save”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “5. Configuring a Reverse Proxy with Nginx
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Why You Need a Reverse Proxy
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Installing and Configuring Nginx
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo nano /etc/nginx/sites-available/myapp”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Paste the following production-ready configuration:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “server {\n listen 80;\n server_name yourdomain.com www.yourdomain.com;\n\n add_header X-Frame-Options \"SAMEORIGIN\" always;\n add_header X-Content-Type-Options \"nosniff\" always;\n add_header X-XSS-Protection \"1; mode=block\" always;\n add_header Referrer-Policy \"no-referrer-when-downgrade\" always;\n\n gzip on;\n gzip_types text/plain application/json application/javascript text/css;\n\n location / {\n proxy_pass http://localhost:3000;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection 'upgrade';\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_cache_bypass $http_upgrade;\n proxy_read_timeout 86400;\n proxy_connect_timeout 60s;\n }\n\n location /static/ {\n alias /var/www/myapp/public/;\n expires 30d;\n add_header Cache-Control \"public, immutable\";\n }\n}”
},
{
“blockName”: “core/code”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/\nsudo nginx -t\nsudo systemctl reload nginx”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “6. Securing Your Deployment
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Setting Up a Firewall with UFW
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo ufw default deny incoming\nsudo ufw default allow outgoing\nsudo ufw allow OpenSSH\nsudo ufw allow 'Nginx Full'\nsudo ufw deny 3000/tcp\nsudo ufw enable\nsudo ufw status verbose”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Securing with SSL \u2014 Let\u2019s Encrypt and Certbot
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo snap install --classic certbot\nsudo ln -s /snap/bin/certbot /usr/bin/certbot\nsudo certbot --nginx -d yourdomain.com -d www.yourdomain.com\nsudo systemctl status snap.certbot.renew.timer\nsudo certbot renew --dry-run”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Blocking Brute-Force Attacks with Fail2ban
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo apt install fail2ban -y\nsudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\nsudo nano /etc/fail2ban/jail.local”
},
{
“blockName”: “core/paragraph”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “Find the [sshd] section and set:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “[sshd]\nenabled = true\nport = ssh\nmaxretry = 3\nbantime = 3600\nfindtime = 600”
},
{
“blockName”: “core/code”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “sudo systemctl enable fail2ban\nsudo systemctl start fail2ban\nsudo fail2ban-client status sshd”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Securing Your Node.js Application Code
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “npm install helmet\nnpm install express-rate-limit”
},
{
“blockName”: “core/code”,
“attrs”: {},
“innerBlocks”: [],
“innerHTML”: “const express = require('express');\nconst helmet = require('helmet');\nconst rateLimit = require('express-rate-limit');\n\nconst app = express();\n\napp.use(helmet());\napp.set('trust proxy', 1);\n\nconst limiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100\n});\napp.use('/api/', limiter);”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “7. Automating Deployment with CI/CD
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “Using GitHub Actions
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Create the following file in your repository: .github/workflows/deploy.yml
name: Deploy Node.js to Linux Server\n\non:\n push:\n branches: [main]\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Setup Node.js v22\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n cache: 'npm'\n - name: Install dependencies\n run: npm ci\n - name: Run tests\n run: npm test\n\n deploy:\n runs-on: ubuntu-latest\n needs: test\n if: github.ref == 'refs/heads/main'\n steps:\n - name: Deploy via SSH\n uses: appleboy/[email protected]\n with:\n host: ${{ secrets.SERVER_HOST }}\n username: ${{ secrets.SERVER_USER }}\n key: ${{ secrets.SSH_PRIVATE_KEY }}\n script: |\n cd /var/www/myapp\n git pull origin main\n npm ci --omit=dev\n pm2 reload ecosystem.config.js --env production\n pm2 save”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Setting Up GitHub Secrets
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Go to your GitHub repository \u2192 Settings \u2192 Secrets and Variables \u2192 Actions \u2192 New Repository Secret and add these three secrets:
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “cat ~/.ssh/id_ed25519)8. Monitoring and Maintenance
” }, { “blockName”: “core/table”, “attrs”: { “hasFixedLayout”: true }, “innerBlocks”: [], “innerHTML”: “| Command | Description |
|---|---|
| pm2 status | Overview of all running apps |
| pm2 monit | Real-time CPU and memory dashboard |
| pm2 logs myapp | Live log stream |
| pm2 logs myapp –lines 200 | View last 200 lines of logs |
| pm2 describe myapp | Full process information |
| pm2 flush | Clear all log files |
Setting Up Log Rotation
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 install pm2-logrotate\npm2 set pm2-logrotate:max_size 10M\npm2 set pm2-logrotate:retain 7\npm2 set pm2-logrotate:compress true”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Zero-Downtime Reloads with PM2
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 reload ecosystem.config.js --env production”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “9. Troubleshooting Common Issues
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “502 Bad Gateway from Nginx
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “This means Nginx is running but your Node.js app is not, or is not listening on the expected port.
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 status\npm2 logs myapp\npm2 restart myapp”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “Port 3000 Already in Use
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “sudo lsof -i :3000\nkill -9 [PID]”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “App Crashes on Reboot
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 startup systemd\npm2 save”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 2 },
“innerBlocks”: [],
“innerHTML”: “Conclusion
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Deploying a Node.js application on Linux in 2026 is a structured, repeatable process. When each layer is correctly configured, the result is a production system that is fast, resilient, and secure. Here is a summary of what you have built through this guide:
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Need a Linux VPS server that is already optimised for Node.js? CloudMinister offers Node.js-ready VPS plans with SSD storage, 99.99% uptime, and 24/7 expert support \u2014 so you can focus on your code, not your infrastructure.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 2 }, “innerBlocks”: [], “innerHTML”: “Frequently Asked Questions \u2013 FAQs
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “1. What is the best way to install Node.js on Linux in 2026?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “The best method is to use NVM (Node Version Manager). Run:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash\nsource ~/.bashrc\nnvm install 22\nnvm use 22\nnvm alias default 22”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “2. How do I keep my Node.js application running after the terminal is closed or the server reboots?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Use PM2. After starting your app, run:
” }, { “blockName”: “core/code”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “pm2 startup systemd\npm2 save”
},
{
“blockName”: “core/heading”,
“attrs”: { “level”: 3 },
“innerBlocks”: [],
“innerHTML”: “3. What is the easiest way to expose my Node.js app to the internet?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Use Nginx as a reverse proxy. After saving the config: sudo nginx -t && sudo systemctl reload nginx
4. How do I automate deployments so I do not have to SSH in every time?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Use GitHub Actions CI/CD. Create a .github/workflows/deploy.yml file in your repository and set three GitHub Secrets (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY). Every push to the main branch will automatically test and deploy your code.
5. How do I secure and optimise my Node.js deployment?
” }, { “blockName”: “core/list”, “attrs”: {}, “innerBlocks”: [ { “blockName”: “core/list-item”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “6. Should I use Docker instead of PM2 for Node.js on Linux in 2026?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Both are valid choices. PM2 is simpler and uses fewer server resources \u2014 ideal for a single Node.js application on a VPS. Docker is better when you need consistent environments across dev, staging, and production, or when running multiple isolated services. For most startups in 2026, PM2 + Nginx on a VPS is the most cost-effective approach.
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “7. Which Node.js version should I use in production in 2026?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Use Node.js v22 LTS (codename \u201cIron\u201d). It is the current Active LTS release, supported until April 2027, and includes the latest performance improvements and security patches. Avoid v18 (End of Life April 2025) and v16 (End of Life September 2023).
” }, { “blockName”: “core/heading”, “attrs”: { “level”: 3 }, “innerBlocks”: [], “innerHTML”: “8. How do I deploy a Node.js app specifically on Ubuntu 24.04?
” }, { “blockName”: “core/paragraph”, “attrs”: {}, “innerBlocks”: [], “innerHTML”: “Ubuntu 24.04 LTS (Noble Numbat) is the recommended OS for Node.js in 2026. The entire process in this guide applies directly to Ubuntu 24.04. One important note: the default Node.js version installed by Ubuntu\u2019s apt repository is v18, which is End of Life. Always install Node.js using NVM or the NodeSource v22 binary instead of running sudo apt install nodejs directly.

He is the CEO and Founder with over a decade of experience in cloud infrastructure, DevOps, and server optimization. With a strong vision and hands-on leadership approach, he has built scalable, secure, and high-performance cloud solutions trusted by businesses across industries.



