This guide shows a clean way to run a production Node.js app on Ubuntu 22.04.
We will use an image that already includes Node.js, add a process manager, place Nginx in front as a reverse proxy, enable HTTPS, harden basic security, and set up a simple deployment flow.
If you still need a server with Node.js ready, see our Node.js VPS hosting page.
What you’ll use
- Ubuntu 22.04 on a fresh VPS
- Node.js and npm already installed by the LifeinCloud Node.js template
- SSH for remote access
- PM2 for process management
- Nginx as a reverse proxy
- Certbot for TLS certificates
Everything here has been tested on Ubuntu 22.04 LTS. Commands assume the root user. If you use a non-root user with sudo, prefix the commands with sudo.
1) Connect and update packages
Open your terminal and connect:
ssh root@YOUR_SERVER_IP
Accept the host key prompt then enter your password or use your SSH key.
Update the system:
apt update && apt upgrade -y
Confirm Node.js is present:
node -v
npm -v
If both print versions, the template is ready.
2) Create a deployment user and secure SSH
Running everything as root is risky. Create a user, add it to the sudo group, and set a password.
adduser deployer
usermod -aG sudo deployer
Copy your SSH key to the new user so you can log in without a password:
rsync -avz ~/.ssh /home/deployer/
chown -R deployer:deployer /home/deployer/.ssh
chmod 700 /home/deployer/.ssh
chmod 600 /home/deployer/.ssh/authorized_keys
Now restrict direct root SSH logins:
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl reload ssh
Open a second terminal and confirm you can log in as the new user:
ssh deployer@YOUR_SERVER_IP
Keep the root session until you verify access.
3) Firewall basics
Enable UFW and allow SSH. Later you will allow web traffic.
ufw allow OpenSSH
ufw enable
ufw status
4) Install PM2 to keep the app alive
PM2 restarts your app if it crashes and on reboot.
npm install -g pm2
pm2 --version
If your app needs a specific Node.js version, install nvm for your user, then nvm install <version>
and nvm use <version>
. The template default suits most projects. You can switch later without changing this guide.
5) Get your code on the server
You can deploy from Git or copy files with SCP. Both are fine.
Option A. Git
cd /home/deployer
git clone https://github.com/yourname/yourproject.git
cd yourproject
npm ci || npm install
Option B. SCP from your laptop
scp -r ./yourproject deployer@YOUR_SERVER_IP:/home/deployer/
cd /home/deployer/yourproject
npm ci || npm install
Give the folder to your user if needed:
chown -R deployer:deployer /home/deployer/yourproject
Create a basic environment file if your app expects one:
cat >/home/deployer/yourproject/.env <<'EOF'
PORT=3000
NODE_ENV=production
EOF
6) Launch with PM2
From the project folder:
pm2 start app.js --name yourapp
pm2 save
pm2 startup
Replace app.js
with your real entry point. The pm2 startup
command prints a systemd line. Run the printed command to finalize auto start.
Check status:
pm2 list
pm2 logs yourapp
If your app listens on port 3000, you can curl it locally:
curl http://localhost:3000/
7) Put Nginx in front as a reverse proxy
Install Nginx:
apt install nginx -y
Allow web traffic in UFW:
ufw allow 'Nginx Full'
Create a site config that proxies to your Node.js port. Replace example.com
with your domain and adjust the upstream port if needed.
cat >/etc/nginx/sites-available/yourapp <<'EOF'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
EOF
ln -s /etc/nginx/sites-available/yourapp /etc/nginx/sites-enabled/yourapp
nginx -t && systemctl reload nginx
Visit http://example.com
. You should see your app without a port number.
8) Add HTTPS with Certbot
Install Certbot and the Nginx plugin:
apt install certbot python3-certbot-nginx -y
Issue a certificate:
certbot --nginx -d example.com -d www.example.com
Certbot edits your Nginx server block to use TLS and sets up auto renewal. You can test renewal:
certbot renew --dry-run
9) Log files and monitoring
Nginx logs:
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
PM2 logs:
pm2 logs yourapp
pm2 monit
System services:
systemctl status nginx
systemctl status pm2-deployer
The PM2 service name follows the user. If you created deployer
, systemd names the unit pm2-deployer
.
10) Zero downtime updates
Pull changes, install packages, reload the process, and you are done.
cd /home/deployer/yourproject
git pull
npm ci || npm install
pm2 reload yourapp
pm2 reload
restarts the app without cutting connections. If you maintain a simple build step, run it before reload.
11) Optional improvements
Swap file for low-memory plans
For small instances a modest swap can improve stability during installs.
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Separate environments
Keep /home/deployer/yourproject
for production and use a second VPS or a second PM2 process for staging with a different port and a subdomain.
Rate limiting at the edge
Nginx rate limiting can help against bursts. Add a limit zone in http {}
then a limit_req
in your server block. Tune carefully to avoid blocking real users.
Backups
Back up your app data and any persistent storage. Database backups should live off the VPS as well as on it.
Troubleshooting
502 Bad Gateway
Nginx cannot reach the Node.js port. Check that the app is listening on the expected address and port. If the app binds to 127.0.0.1:3000
, your proxy must point to that exact pair. Confirm with ss -ltnp | grep 3000
.
Permission denied on ports
The Node.js app must not bind to port 80 or 443 directly. Let Nginx listen on those ports then proxy to your app’s high port.
PM2 not starting on boot
Re-run pm2 startup
, execute the printed systemd command, then pm2 save
. Confirm with systemctl is-enabled pm2-deployer
.
TLS certificate fails
Make sure DNS A and AAAA records point to the server. Wait for DNS propagation, then run Certbot again.
Large uploads stall
Increase Nginx client body size inside the server block:
client_max_body_size 20m;
Reload Nginx after edits.
FAQ
How do I change the Node.js version without breaking the app?
Install nvm for your user, install the target version, set it as default, then restart the PM2 process. Keep the old version until you confirm runtime compatibility.
How do I run several Node.js apps behind one Nginx?
Give each app its own PM2 name and port. Create separate Nginx server blocks per domain or subdomain that proxy to the right port. Certbot can issue certificates for each.
How do I serve static files efficiently?
Let your Node.js framework serve small assets or configure Nginx to serve a build folder directly with a location /assets
rule. Keep cache headers consistent with your app’s versioning.
How do I add a domain with www and apex?
Create two DNS records pointing to the server IP. In Nginx, set both names in server_name
. Certbot can issue a single certificate for both.
How do I back up app data?
Back up your repository and any external data stores. If you keep user uploads on disk, archive and ship them to remote storage on a schedule. For databases use vendor tools and test restores.
Helpful references
Official Node.js downloads and docs
PM2 process manager documentation
Nginx official documentation
Certbot instructions by OS and web server