Ravindra BagaleCourses & study guides मराठी Track your progress

Chapter 11: Application Hosting — WordPress, Flask and Express

11.3 Express application with pm2 + Nginx

It's really simple – once you have done Flask, Express follows exactly the same idea: app on localhost, a process manager to keep it alive, Nginx in front. Here pm2 takes the place of Gunicorn.

Why pm2?

Friends, when you close SSH, node app.js stops – every student has this experience. pm2 keeps the app running and starts it again after a crash.

If you start Node with node app.js and close the SSH session, the app dies. If it crashes, nobody restarts it. pm2 is a production process manager for Node.js that keeps apps alive, restarts on crash, starts them at boot, manages logs, and can run one process per CPU core (cluster mode) with zero-downtime reloads.

Step 1 — Install Node.js, pm2 and Nginx

# AL2023 / CentOS Stream 9
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo yum install -y nodejs nginx git
# Ubuntu
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs nginx git

sudo npm install -g pm2

Step 2 — Create the Express app

sudo mkdir -p /opt/expressdemo/public && sudo chown -R $USER:$USER /opt/expressdemo
cd /opt/expressdemo
npm init -y
npm install express

cat > server.js <<'EOF'
const express = require("express");
const os = require("os");
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use(express.static("public"));

const todos = [{ id: 1, task: "Learn EC2" }, { id: 2, task: "Deploy with pm2" }];

app.get("/api/todos", (req, res) => res.json(todos));
app.post("/api/todos", (req, res) => {
  const todo = { id: todos.length + 1, task: req.body.task };
  todos.push(todo);
  res.status(201).json(todo);
});
app.get("/api/health", (req, res) => res.json({ status: "ok", host: os.hostname(), pid: process.pid }));

app.listen(PORT, "127.0.0.1", () => console.log(`Express running on 127.0.0.1:${PORT}`));
EOF

cat > public/index.html <<'EOF'
<!DOCTYPE html><html><head><meta charset="utf-8"><title>Express on EC2</title></head>
<body style="font-family:Arial;max-width:600px;margin:40px auto">
<h1>Express + pm2 + Nginx</h1><ul id="list"></ul>
<script>
fetch("/api/todos").then(r => r.json()).then(items => {
  document.getElementById("list").innerHTML = items.map(t => "<li>" + t.task + "</li>").join("");
});
</script></body></html>
EOF

Step 3 — pm2 ecosystem file (cluster mode)

cat > ecosystem.config.js <<'EOF'
module.exports = {
  apps: [{
    name: "expressdemo",
    script: "server.js",
    cwd: "/opt/expressdemo",
    instances: "max",          // one process per vCPU
    exec_mode: "cluster",
    env: { NODE_ENV: "production", PORT: 3000 },
    max_memory_restart: "300M"
  }]
};
EOF

pm2 start ecosystem.config.js
pm2 status
curl -s http://127.0.0.1:3000/api/health
pm2 startup systemd        # copy and run the 'sudo env PATH=...' line it prints
pm2 save

Useful pm2 commands

Command Purpose
pm2 list / pm2 status Show processes
pm2 logs expressdemo Stream logs (--lines 100)
pm2 monit Live CPU/memory dashboard
pm2 restart expressdemo Restart (brief downtime)
pm2 reload expressdemo Zero-downtime reload (cluster mode)
pm2 stop / pm2 delete expressdemo Stop / remove from list
pm2 save Save process list for resurrection at boot
pm2 install pm2-​logrotate Rotate logs so the disk doesn't fill up

Step 4 — Nginx reverse proxy

# AL2023 / CentOS path; Ubuntu: /etc/nginx/sites-available/expressdemo + symlink + remove default
sudo tee /etc/nginx/conf.d/expressdemo.conf > /dev/null <<'EOF'
upstream express_backend {
    server 127.0.0.1:3000;
    keepalive 16;
}
server {
    listen 80;
    server_name YOUR_SERVER_NAME;

    location / {
        proxy_pass http://express_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;
    }
}
EOF
sudo sed -i "s/YOUR_SERVER_NAME/$(curl -s https://checkip.amazonaws.com)/" /etc/nginx/conf.d/expressdemo.conf
sudo setsebool -P httpd_can_network_connect 1 2>/dev/null   # CentOS only
sudo service nginx start
sudo systemctl enable nginx
sudo nginx -t && sudo service nginx reload

Browse to http://<PUBLIC_IP>/ and http://<PUBLIC_IP>/api/health (refresh a few times — the pid changes because requests are shared across cluster workers).

Step 5 — Deploying updates

cd /opt/expressdemo
git pull
npm ci --omit=dev           # clean install exactly from package-lock.json
pm2 reload expressdemo      # zero downtime

Ravindra Bagale's Tip

Many students run pm2 startup but not the sudo env PATH=... command it prints, and after a reboot the app is gone. After that command, be sure to run pm2 save as well. For updates use pm2 reload expressdemo instead of pm2 restart – no downtime.

Lab

Chala, Express demo pm2 cluster mode madhe chalu kara aani http://<PUBLIC_IP>/api/health kahi vela refresh karun pid badalto ka te liha. Mag sudo reboot kara aani parat login karun pm2 status ne app aapoaap parat aala ka te confirm kara.